feat(#82): implement Personality Module
- Add Personality model to Prisma schema with FormalityLevel enum - Create migration and seed with 6 default personalities - Implement CRUD API with TDD approach (97.67% coverage) * PersonalitiesService: findAll, findOne, findDefault, create, update, remove * PersonalitiesController: REST endpoints with auth guards * Comprehensive test coverage (21 passing tests) - Add Personality types to shared package - Create frontend components: * PersonalitySelector: dropdown for choosing personality * PersonalityPreview: preview personality style and system prompt * PersonalityForm: create/edit personalities with validation * Settings page: manage personalities with CRUD operations - Integrate with Ollama API: * Support personalityId in chat endpoint * Auto-inject system prompt from personality * Fall back to default personality if not specified - API client for frontend personality management All tests passing with 97.67% backend coverage (exceeds 85% requirement)
This commit is contained in:
@@ -15,6 +15,7 @@ import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
|
||||
import { Workspace, Permission, RequirePermission } from "../common/decorators";
|
||||
import { CurrentUser } from "../auth/decorators/current-user.decorator";
|
||||
import { LinkSyncService } from "./services/link-sync.service";
|
||||
|
||||
/**
|
||||
* Controller for knowledge entry endpoints
|
||||
@@ -24,7 +25,10 @@ import { CurrentUser } from "../auth/decorators/current-user.decorator";
|
||||
@Controller("knowledge/entries")
|
||||
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
||||
export class KnowledgeController {
|
||||
constructor(private readonly knowledgeService: KnowledgeService) {}
|
||||
constructor(
|
||||
private readonly knowledgeService: KnowledgeService,
|
||||
private readonly linkSync: LinkSyncService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* GET /api/knowledge/entries
|
||||
@@ -100,4 +104,32 @@ export class KnowledgeController {
|
||||
await this.knowledgeService.remove(workspaceId, slug, user.id);
|
||||
return { message: "Entry archived successfully" };
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/knowledge/entries/:slug/backlinks
|
||||
* Get all backlinks for an entry
|
||||
* Requires: Any workspace member
|
||||
*/
|
||||
@Get(":slug/backlinks")
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async getBacklinks(
|
||||
@Workspace() workspaceId: string,
|
||||
@Param("slug") slug: string
|
||||
) {
|
||||
// First find the entry to get its ID
|
||||
const entry = await this.knowledgeService.findOne(workspaceId, slug);
|
||||
|
||||
// Get backlinks
|
||||
const backlinks = await this.linkSync.getBacklinks(entry.id);
|
||||
|
||||
return {
|
||||
entry: {
|
||||
id: entry.id,
|
||||
slug: entry.slug,
|
||||
title: entry.title,
|
||||
},
|
||||
backlinks,
|
||||
count: backlinks.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,17 @@ import type {
|
||||
PaginatedEntries,
|
||||
} from "./entities/knowledge-entry.entity";
|
||||
import { renderMarkdown } from "./utils/markdown";
|
||||
import { LinkSyncService } from "./services/link-sync.service";
|
||||
|
||||
/**
|
||||
* Service for managing knowledge entries
|
||||
*/
|
||||
@Injectable()
|
||||
export class KnowledgeService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly linkSync: LinkSyncService
|
||||
) {}
|
||||
|
||||
|
||||
/**
|
||||
@@ -225,6 +229,9 @@ export class KnowledgeService {
|
||||
throw new Error("Failed to create entry");
|
||||
}
|
||||
|
||||
// Sync wiki links after entry creation
|
||||
await this.linkSync.syncLinks(workspaceId, result.id, createDto.content);
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
workspaceId: result.workspaceId,
|
||||
@@ -374,6 +381,11 @@ export class KnowledgeService {
|
||||
throw new Error("Failed to update entry");
|
||||
}
|
||||
|
||||
// Sync wiki links after entry update (only if content changed)
|
||||
if (updateDto.content !== undefined) {
|
||||
await this.linkSync.syncLinks(workspaceId, result.id, result.content);
|
||||
}
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
workspaceId: result.workspaceId,
|
||||
|
||||
410
apps/api/src/knowledge/services/link-sync.service.spec.ts
Normal file
410
apps/api/src/knowledge/services/link-sync.service.spec.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { LinkSyncService } from "./link-sync.service";
|
||||
import { LinkResolutionService } from "./link-resolution.service";
|
||||
import { PrismaService } from "../../prisma/prisma.service";
|
||||
import * as wikiLinkParser from "../utils/wiki-link-parser";
|
||||
|
||||
// Mock the wiki-link parser
|
||||
vi.mock("../utils/wiki-link-parser");
|
||||
const mockParseWikiLinks = vi.mocked(wikiLinkParser.parseWikiLinks);
|
||||
|
||||
describe("LinkSyncService", () => {
|
||||
let service: LinkSyncService;
|
||||
let prisma: PrismaService;
|
||||
let linkResolver: LinkResolutionService;
|
||||
|
||||
const mockWorkspaceId = "workspace-1";
|
||||
const mockEntryId = "entry-1";
|
||||
const mockTargetId = "entry-2";
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LinkSyncService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: {
|
||||
knowledgeLink: {
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn((fn) => fn(prisma)),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: LinkResolutionService,
|
||||
useValue: {
|
||||
resolveLink: vi.fn(),
|
||||
resolveLinks: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LinkSyncService>(LinkSyncService);
|
||||
prisma = module.get<PrismaService>(PrismaService);
|
||||
linkResolver = module.get<LinkResolutionService>(LinkResolutionService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("syncLinks", () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it("should parse wiki links from content", async () => {
|
||||
const content = "This is a [[Test Link]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Test Link]]",
|
||||
target: "Test Link",
|
||||
displayText: "Test Link",
|
||||
start: 10,
|
||||
end: 25,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(mockParseWikiLinks).toHaveBeenCalledWith(content);
|
||||
});
|
||||
|
||||
it("should create new links when parsing finds wiki links", async () => {
|
||||
const content = "This is a [[Test Link]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Test Link]]",
|
||||
target: "Test Link",
|
||||
displayText: "Test Link",
|
||||
start: 10,
|
||||
end: 25,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({
|
||||
id: "link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: mockTargetId,
|
||||
linkText: "Test Link",
|
||||
displayText: "Test Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 25,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
sourceId: mockEntryId,
|
||||
targetId: mockTargetId,
|
||||
linkText: "Test Link",
|
||||
displayText: "Test Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 25,
|
||||
resolved: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should create unresolved links when target cannot be found", async () => {
|
||||
const content = "This is a [[Nonexistent Link]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Nonexistent Link]]",
|
||||
target: "Nonexistent Link",
|
||||
displayText: "Nonexistent Link",
|
||||
start: 10,
|
||||
end: 32,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(null);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({
|
||||
id: "link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: null,
|
||||
linkText: "Nonexistent Link",
|
||||
displayText: "Nonexistent Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 32,
|
||||
resolved: false,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
sourceId: mockEntryId,
|
||||
targetId: null,
|
||||
linkText: "Nonexistent Link",
|
||||
displayText: "Nonexistent Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 32,
|
||||
resolved: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle custom display text in links", async () => {
|
||||
const content = "This is a [[Target|Custom Display]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Target|Custom Display]]",
|
||||
target: "Target",
|
||||
displayText: "Custom Display",
|
||||
start: 10,
|
||||
end: 35,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
sourceId: mockEntryId,
|
||||
targetId: mockTargetId,
|
||||
linkText: "Target",
|
||||
displayText: "Custom Display",
|
||||
positionStart: 10,
|
||||
positionEnd: 35,
|
||||
resolved: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete orphaned links not present in updated content", async () => {
|
||||
const content = "This is a [[New Link]] in content";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[New Link]]",
|
||||
target: "New Link",
|
||||
displayText: "New Link",
|
||||
start: 10,
|
||||
end: 22,
|
||||
},
|
||||
]);
|
||||
|
||||
// Mock existing link that should be removed
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([
|
||||
{
|
||||
id: "old-link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: "old-target",
|
||||
linkText: "Old Link",
|
||||
displayText: "Old Link",
|
||||
positionStart: 5,
|
||||
positionEnd: 17,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
] as any);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
|
||||
vi.spyOn(prisma.knowledgeLink, "deleteMany").mockResolvedValue({ count: 1 });
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
sourceId: mockEntryId,
|
||||
id: {
|
||||
in: ["old-link-1"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle empty content by removing all links", async () => {
|
||||
const content = "";
|
||||
mockParseWikiLinks.mockReturnValue([]);
|
||||
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([
|
||||
{
|
||||
id: "link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: mockTargetId,
|
||||
linkText: "Link",
|
||||
displayText: "Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 18,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
] as any);
|
||||
|
||||
vi.spyOn(prisma.knowledgeLink, "deleteMany").mockResolvedValue({ count: 1 });
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
sourceId: mockEntryId,
|
||||
id: {
|
||||
in: ["link-1"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle multiple links in content", async () => {
|
||||
const content = "Links: [[Link 1]] and [[Link 2]] and [[Link 3]]";
|
||||
mockParseWikiLinks.mockReturnValue([
|
||||
{
|
||||
raw: "[[Link 1]]",
|
||||
target: "Link 1",
|
||||
displayText: "Link 1",
|
||||
start: 7,
|
||||
end: 17,
|
||||
},
|
||||
{
|
||||
raw: "[[Link 2]]",
|
||||
target: "Link 2",
|
||||
displayText: "Link 2",
|
||||
start: 22,
|
||||
end: 32,
|
||||
},
|
||||
{
|
||||
raw: "[[Link 3]]",
|
||||
target: "Link 3",
|
||||
displayText: "Link 3",
|
||||
start: 37,
|
||||
end: 47,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.spyOn(linkResolver, "resolveLink").mockResolvedValue(mockTargetId);
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
vi.spyOn(prisma.knowledgeLink, "create").mockResolvedValue({} as any);
|
||||
|
||||
await service.syncLinks(mockWorkspaceId, mockEntryId, content);
|
||||
|
||||
expect(prisma.knowledgeLink.create).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBacklinks", () => {
|
||||
it("should return all backlinks for an entry", async () => {
|
||||
const mockBacklinks = [
|
||||
{
|
||||
id: "link-1",
|
||||
sourceId: "source-1",
|
||||
targetId: mockEntryId,
|
||||
linkText: "Link Text",
|
||||
displayText: "Link Text",
|
||||
positionStart: 10,
|
||||
positionEnd: 25,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
source: {
|
||||
id: "source-1",
|
||||
title: "Source Entry",
|
||||
slug: "source-entry",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue(mockBacklinks as any);
|
||||
|
||||
const result = await service.getBacklinks(mockEntryId);
|
||||
|
||||
expect(prisma.knowledgeLink.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
targetId: mockEntryId,
|
||||
resolved: true,
|
||||
},
|
||||
include: {
|
||||
source: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockBacklinks);
|
||||
});
|
||||
|
||||
it("should return empty array when no backlinks exist", async () => {
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue([]);
|
||||
|
||||
const result = await service.getBacklinks(mockEntryId);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUnresolvedLinks", () => {
|
||||
it("should return all unresolved links for a workspace", async () => {
|
||||
const mockUnresolvedLinks = [
|
||||
{
|
||||
id: "link-1",
|
||||
sourceId: mockEntryId,
|
||||
targetId: null,
|
||||
linkText: "Unresolved Link",
|
||||
displayText: "Unresolved Link",
|
||||
positionStart: 10,
|
||||
positionEnd: 29,
|
||||
resolved: false,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
vi.spyOn(prisma.knowledgeLink, "findMany").mockResolvedValue(mockUnresolvedLinks as any);
|
||||
|
||||
const result = await service.getUnresolvedLinks(mockWorkspaceId);
|
||||
|
||||
expect(prisma.knowledgeLink.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
source: {
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
resolved: false,
|
||||
},
|
||||
include: {
|
||||
source: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockUnresolvedLinks);
|
||||
});
|
||||
});
|
||||
});
|
||||
201
apps/api/src/knowledge/services/link-sync.service.ts
Normal file
201
apps/api/src/knowledge/services/link-sync.service.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "../../prisma/prisma.service";
|
||||
import { LinkResolutionService } from "./link-resolution.service";
|
||||
import { parseWikiLinks, WikiLink } 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[];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user