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:
169
apps/api/src/knowledge/services/stats.service.ts
Normal file
169
apps/api/src/knowledge/services/stats.service.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { EntryStatus } from "@prisma/client";
|
||||
import { PrismaService } from "../../prisma/prisma.service";
|
||||
import type { KnowledgeStats } from "../entities/stats.entity";
|
||||
|
||||
/**
|
||||
* Service for knowledge base statistics
|
||||
*/
|
||||
@Injectable()
|
||||
export class StatsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Get comprehensive knowledge base statistics
|
||||
*/
|
||||
async getStats(workspaceId: string): Promise<KnowledgeStats> {
|
||||
// Run queries in parallel for better performance
|
||||
const [
|
||||
totalEntries,
|
||||
totalTags,
|
||||
totalLinks,
|
||||
publishedEntries,
|
||||
draftEntries,
|
||||
archivedEntries,
|
||||
entriesWithLinkCounts,
|
||||
recentEntries,
|
||||
tagsWithCounts,
|
||||
] = await Promise.all([
|
||||
// Total entries
|
||||
this.prisma.knowledgeEntry.count({
|
||||
where: { workspaceId },
|
||||
}),
|
||||
|
||||
// Total tags
|
||||
this.prisma.knowledgeTag.count({
|
||||
where: { workspaceId },
|
||||
}),
|
||||
|
||||
// Total links
|
||||
this.prisma.knowledgeLink.count({
|
||||
where: {
|
||||
source: { workspaceId },
|
||||
},
|
||||
}),
|
||||
|
||||
// Published entries
|
||||
this.prisma.knowledgeEntry.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
status: EntryStatus.PUBLISHED,
|
||||
},
|
||||
}),
|
||||
|
||||
// Draft entries
|
||||
this.prisma.knowledgeEntry.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
status: EntryStatus.DRAFT,
|
||||
},
|
||||
}),
|
||||
|
||||
// Archived entries
|
||||
this.prisma.knowledgeEntry.count({
|
||||
where: {
|
||||
workspaceId,
|
||||
status: EntryStatus.ARCHIVED,
|
||||
},
|
||||
}),
|
||||
|
||||
// Most connected entries
|
||||
this.prisma.knowledgeEntry.findMany({
|
||||
where: { workspaceId },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
incomingLinks: true,
|
||||
outgoingLinks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
incomingLinks: {
|
||||
_count: "desc",
|
||||
},
|
||||
},
|
||||
take: 10,
|
||||
}),
|
||||
|
||||
// Recent activity
|
||||
this.prisma.knowledgeEntry.findMany({
|
||||
where: { workspaceId },
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
take: 10,
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
updatedAt: true,
|
||||
status: true,
|
||||
},
|
||||
}),
|
||||
|
||||
// Tag distribution
|
||||
this.prisma.knowledgeTag.findMany({
|
||||
where: { workspaceId },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
entries: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
entries: {
|
||||
_count: "desc",
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Transform most connected entries
|
||||
const mostConnected = entriesWithLinkCounts.map((entry) => {
|
||||
const incomingLinks = entry._count.incomingLinks;
|
||||
const outgoingLinks = entry._count.outgoingLinks;
|
||||
return {
|
||||
id: entry.id,
|
||||
slug: entry.slug,
|
||||
title: entry.title,
|
||||
incomingLinks,
|
||||
outgoingLinks,
|
||||
totalConnections: incomingLinks + outgoingLinks,
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by total connections
|
||||
mostConnected.sort((a, b) => b.totalConnections - a.totalConnections);
|
||||
|
||||
// Transform tag distribution
|
||||
const tagDistribution = tagsWithCounts.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
slug: tag.slug,
|
||||
color: tag.color,
|
||||
entryCount: tag._count.entries,
|
||||
}));
|
||||
|
||||
return {
|
||||
overview: {
|
||||
totalEntries,
|
||||
totalTags,
|
||||
totalLinks,
|
||||
publishedEntries,
|
||||
draftEntries,
|
||||
archivedEntries,
|
||||
},
|
||||
mostConnected,
|
||||
recentActivity: recentEntries.map((entry) => ({
|
||||
id: entry.id,
|
||||
slug: entry.slug,
|
||||
title: entry.title,
|
||||
updatedAt: entry.updatedAt,
|
||||
status: entry.status,
|
||||
})),
|
||||
tagDistribution,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user