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
170 lines
3.8 KiB
TypeScript
170 lines
3.8 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|