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
127 lines
3.5 KiB
TypeScript
127 lines
3.5 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|