Implemented three new API endpoints for knowledge graph visualization: 1. GET /api/knowledge/graph - Full knowledge graph - Returns all entries and links with optional filtering - Supports filtering by tags, status, and node count limit - Includes orphan detection (entries with no links) 2. GET /api/knowledge/graph/stats - Graph statistics - Total entries and links counts - Orphan entries detection - Average links per entry - Top 10 most connected entries - Tag distribution across entries 3. GET /api/knowledge/graph/:slug - Entry-centered subgraph - Returns graph centered on specific entry - Supports depth parameter (1-5) for traversal distance - Includes all connected nodes up to specified depth New Files: - apps/api/src/knowledge/graph.controller.ts - apps/api/src/knowledge/graph.controller.spec.ts Modified Files: - apps/api/src/knowledge/dto/graph-query.dto.ts (added GraphFilterDto) - apps/api/src/knowledge/entities/graph.entity.ts (extended with new types) - apps/api/src/knowledge/services/graph.service.ts (added new methods) - apps/api/src/knowledge/services/graph.service.spec.ts (added tests) - apps/api/src/knowledge/knowledge.module.ts (registered controller) - apps/api/src/knowledge/dto/index.ts (exported new DTOs) - docs/scratchpads/71-graph-data-api.md (implementation notes) Test Coverage: 21 tests (all passing) - 14 service tests including orphan detection, filtering, statistics - 7 controller tests for all three endpoints Follows TDD principles with tests written before implementation. All code quality gates passed (lint, typecheck, tests). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
148 lines
3.7 KiB
TypeScript
148 lines
3.7 KiB
TypeScript
import { Injectable, Logger } from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import { simpleGit, SimpleGit } from "simple-git";
|
|
import { GitOperationError } from "./types";
|
|
|
|
/**
|
|
* Service for managing git operations
|
|
*/
|
|
@Injectable()
|
|
export class GitOperationsService {
|
|
private readonly logger = new Logger(GitOperationsService.name);
|
|
private readonly gitUserName: string;
|
|
private readonly gitUserEmail: string;
|
|
|
|
constructor(private readonly configService: ConfigService) {
|
|
this.gitUserName =
|
|
this.configService.get<string>("orchestrator.git.userName") ??
|
|
"Mosaic Orchestrator";
|
|
this.gitUserEmail =
|
|
this.configService.get<string>("orchestrator.git.userEmail") ??
|
|
"orchestrator@mosaicstack.dev";
|
|
}
|
|
|
|
/**
|
|
* Get a simple-git instance for a local path
|
|
*/
|
|
private getGit(localPath: string): SimpleGit {
|
|
return simpleGit(localPath);
|
|
}
|
|
|
|
/**
|
|
* Clone a repository
|
|
*/
|
|
async cloneRepository(
|
|
url: string,
|
|
localPath: string,
|
|
branch?: string,
|
|
): Promise<void> {
|
|
try {
|
|
this.logger.log(`Cloning repository ${url} to ${localPath}`);
|
|
const git = simpleGit();
|
|
|
|
if (branch) {
|
|
await git.clone(url, localPath, ["--branch", branch]);
|
|
} else {
|
|
await git.clone(url, localPath);
|
|
}
|
|
|
|
this.logger.log(`Successfully cloned repository to ${localPath}`);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to clone repository: ${error}`);
|
|
throw new GitOperationError(
|
|
`Failed to clone repository from ${url}`,
|
|
"clone",
|
|
error as Error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new branch
|
|
*/
|
|
async createBranch(localPath: string, branchName: string): Promise<void> {
|
|
try {
|
|
this.logger.log(`Creating branch ${branchName} at ${localPath}`);
|
|
const git = this.getGit(localPath);
|
|
|
|
await git.checkoutLocalBranch(branchName);
|
|
|
|
this.logger.log(`Successfully created branch ${branchName}`);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to create branch: ${error}`);
|
|
throw new GitOperationError(
|
|
`Failed to create branch ${branchName}`,
|
|
"createBranch",
|
|
error as Error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Commit changes
|
|
*/
|
|
async commit(
|
|
localPath: string,
|
|
message: string,
|
|
files?: string[],
|
|
): Promise<void> {
|
|
try {
|
|
this.logger.log(`Committing changes at ${localPath}`);
|
|
const git = this.getGit(localPath);
|
|
|
|
// Configure git user
|
|
await git.addConfig("user.name", this.gitUserName);
|
|
await git.addConfig("user.email", this.gitUserEmail);
|
|
|
|
// Stage files
|
|
if (files && files.length > 0) {
|
|
await git.add(files);
|
|
} else {
|
|
await git.add(".");
|
|
}
|
|
|
|
// Commit
|
|
await git.commit(message);
|
|
|
|
this.logger.log(`Successfully committed changes: ${message}`);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to commit: ${error}`);
|
|
throw new GitOperationError(
|
|
`Failed to commit changes`,
|
|
"commit",
|
|
error as Error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Push changes to remote
|
|
*/
|
|
async push(
|
|
localPath: string,
|
|
remote: string = "origin",
|
|
branch?: string,
|
|
force: boolean = false,
|
|
): Promise<void> {
|
|
try {
|
|
this.logger.log(`Pushing changes from ${localPath} to ${remote}`);
|
|
const git = this.getGit(localPath);
|
|
|
|
if (force) {
|
|
await git.push(remote, branch, { "--force": null });
|
|
} else {
|
|
await git.push(remote, branch);
|
|
}
|
|
|
|
this.logger.log(`Successfully pushed changes to ${remote}`);
|
|
} catch (error) {
|
|
this.logger.error(`Failed to push: ${error}`);
|
|
throw new GitOperationError(
|
|
`Failed to push changes to ${remote}`,
|
|
"push",
|
|
error as Error,
|
|
);
|
|
}
|
|
}
|
|
}
|