Issue #73 - Entry-Centered Graph View: - Added GET /api/knowledge/entries/:id/graph endpoint with depth parameter - Returns entry + connected nodes with link relationships - Created GraphService for graph traversal using BFS - Added EntryGraphViewer component for frontend - Integrated graph view tab into entry detail page Issue #74 - Graph Statistics Dashboard: - Added GET /api/knowledge/stats endpoint - Returns overview stats (entries, tags, links by status) - Includes most connected entries, recent activity, tag distribution - Created StatsDashboard component with visual stats - Added route at /knowledge/stats Backend: - GraphService: BFS-based graph traversal with configurable depth - StatsService: Parallel queries for comprehensive statistics - GraphQueryDto: Validation for depth parameter (1-5) - Entity types for graph nodes/edges and statistics - Unit tests for both services Frontend: - EntryGraphViewer: Entry-centered graph visualization - StatsDashboard: Statistics overview with charts - Graph view tab on entry detail page - API client functions for new endpoints - TypeScript strict typing throughout
This commit is contained in:
126
apps/api/src/knowledge/services/graph.service.spec.ts
Normal file
126
apps/api/src/knowledge/services/graph.service.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { NotFoundException } from "@nestjs/common";
|
||||
import { GraphService } from "./graph.service";
|
||||
import { PrismaService } from "../../prisma/prisma.service";
|
||||
|
||||
describe("GraphService", () => {
|
||||
let service: GraphService;
|
||||
let prisma: PrismaService;
|
||||
|
||||
const mockEntry = {
|
||||
id: "entry-1",
|
||||
workspaceId: "workspace-1",
|
||||
slug: "test-entry",
|
||||
title: "Test Entry",
|
||||
content: "Test content",
|
||||
contentHtml: "<p>Test content</p>",
|
||||
summary: "Test summary",
|
||||
status: "PUBLISHED",
|
||||
visibility: "WORKSPACE",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdBy: "user-1",
|
||||
updatedBy: "user-1",
|
||||
tags: [],
|
||||
outgoingLinks: [],
|
||||
incomingLinks: [],
|
||||
};
|
||||
|
||||
const mockPrismaService = {
|
||||
knowledgeEntry: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
GraphService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<GraphService>(GraphService);
|
||||
prisma = module.get<PrismaService>(PrismaService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe("getEntryGraph", () => {
|
||||
it("should throw NotFoundException if entry does not exist", async () => {
|
||||
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.getEntryGraph("workspace-1", "non-existent", 1)
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it("should throw NotFoundException if entry belongs to different workspace", async () => {
|
||||
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue({
|
||||
...mockEntry,
|
||||
workspaceId: "different-workspace",
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.getEntryGraph("workspace-1", "entry-1", 1)
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it("should return graph with center node when depth is 0", async () => {
|
||||
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValue(mockEntry);
|
||||
|
||||
const result = await service.getEntryGraph("workspace-1", "entry-1", 0);
|
||||
|
||||
expect(result.centerNode.id).toBe("entry-1");
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
expect(result.edges).toHaveLength(0);
|
||||
expect(result.stats.totalNodes).toBe(1);
|
||||
expect(result.stats.totalEdges).toBe(0);
|
||||
});
|
||||
|
||||
it("should build graph with connected nodes at depth 1", async () => {
|
||||
const linkedEntry = {
|
||||
id: "entry-2",
|
||||
slug: "linked-entry",
|
||||
title: "Linked Entry",
|
||||
summary: null,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
mockPrismaService.knowledgeEntry.findUnique
|
||||
.mockResolvedValueOnce({
|
||||
...mockEntry,
|
||||
outgoingLinks: [
|
||||
{
|
||||
id: "link-1",
|
||||
sourceId: "entry-1",
|
||||
targetId: "entry-2",
|
||||
linkText: "link to entry 2",
|
||||
target: linkedEntry,
|
||||
},
|
||||
],
|
||||
incomingLinks: [],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
...linkedEntry,
|
||||
tags: [],
|
||||
outgoingLinks: [],
|
||||
incomingLinks: [],
|
||||
});
|
||||
|
||||
const result = await service.getEntryGraph("workspace-1", "entry-1", 1);
|
||||
|
||||
expect(result.nodes).toHaveLength(2);
|
||||
expect(result.edges).toHaveLength(1);
|
||||
expect(result.stats.totalNodes).toBe(2);
|
||||
expect(result.stats.totalEdges).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user