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:
122
apps/api/src/knowledge/services/stats.service.spec.ts
Normal file
122
apps/api/src/knowledge/services/stats.service.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { StatsService } from "./stats.service";
|
||||
import { PrismaService } from "../../prisma/prisma.service";
|
||||
import { EntryStatus } from "@prisma/client";
|
||||
|
||||
describe("StatsService", () => {
|
||||
let service: StatsService;
|
||||
let prisma: PrismaService;
|
||||
|
||||
const mockPrismaService = {
|
||||
knowledgeEntry: {
|
||||
count: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
knowledgeTag: {
|
||||
count: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
knowledgeLink: {
|
||||
count: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
StatsService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<StatsService>(StatsService);
|
||||
prisma = module.get<PrismaService>(PrismaService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe("getStats", () => {
|
||||
it("should return knowledge base statistics", async () => {
|
||||
// Mock all the parallel queries
|
||||
mockPrismaService.knowledgeEntry.count
|
||||
.mockResolvedValueOnce(10) // total entries
|
||||
.mockResolvedValueOnce(5) // published
|
||||
.mockResolvedValueOnce(3) // drafts
|
||||
.mockResolvedValueOnce(2); // archived
|
||||
|
||||
mockPrismaService.knowledgeTag.count.mockResolvedValue(7);
|
||||
mockPrismaService.knowledgeLink.count.mockResolvedValue(15);
|
||||
|
||||
mockPrismaService.knowledgeEntry.findMany
|
||||
.mockResolvedValueOnce([
|
||||
// most connected
|
||||
{
|
||||
id: "entry-1",
|
||||
slug: "test-entry",
|
||||
title: "Test Entry",
|
||||
_count: { incomingLinks: 5, outgoingLinks: 3 },
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
// recent activity
|
||||
{
|
||||
id: "entry-2",
|
||||
slug: "recent-entry",
|
||||
title: "Recent Entry",
|
||||
updatedAt: new Date(),
|
||||
status: EntryStatus.PUBLISHED,
|
||||
},
|
||||
]);
|
||||
|
||||
mockPrismaService.knowledgeTag.findMany.mockResolvedValue([
|
||||
{
|
||||
id: "tag-1",
|
||||
name: "Test Tag",
|
||||
slug: "test-tag",
|
||||
color: "#FF0000",
|
||||
_count: { entries: 3 },
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.getStats("workspace-1");
|
||||
|
||||
expect(result.overview.totalEntries).toBe(10);
|
||||
expect(result.overview.totalTags).toBe(7);
|
||||
expect(result.overview.totalLinks).toBe(15);
|
||||
expect(result.overview.publishedEntries).toBe(5);
|
||||
expect(result.overview.draftEntries).toBe(3);
|
||||
expect(result.overview.archivedEntries).toBe(2);
|
||||
|
||||
expect(result.mostConnected).toHaveLength(1);
|
||||
expect(result.mostConnected[0].totalConnections).toBe(8);
|
||||
|
||||
expect(result.recentActivity).toHaveLength(1);
|
||||
expect(result.tagDistribution).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should handle empty knowledge base", async () => {
|
||||
// Mock all counts as 0
|
||||
mockPrismaService.knowledgeEntry.count.mockResolvedValue(0);
|
||||
mockPrismaService.knowledgeTag.count.mockResolvedValue(0);
|
||||
mockPrismaService.knowledgeLink.count.mockResolvedValue(0);
|
||||
mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([]);
|
||||
mockPrismaService.knowledgeTag.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getStats("workspace-1");
|
||||
|
||||
expect(result.overview.totalEntries).toBe(0);
|
||||
expect(result.overview.totalTags).toBe(0);
|
||||
expect(result.overview.totalLinks).toBe(0);
|
||||
expect(result.mostConnected).toHaveLength(0);
|
||||
expect(result.recentActivity).toHaveLength(0);
|
||||
expect(result.tagDistribution).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user