feat(#71): implement graph data API
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>
This commit is contained in:
147
apps/orchestrator/src/git/git-operations.service.ts
Normal file
147
apps/orchestrator/src/git/git-operations.service.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user