@memory-bank/mcp
Version:
Memory-enabled Co-Pilot (MCP) server for managing project documentation and context
419 lines • 18.7 kB
JavaScript
import path from "path";
import { BranchInfo } from "../../../domain/entities/BranchInfo.js";
import { DocumentId } from "../../../domain/entities/DocumentId.js";
import { DocumentPath } from "../../../domain/entities/DocumentPath.js";
import { JsonDocument } from "../../../domain/entities/JsonDocument.js";
import { InfrastructureError, InfrastructureErrorCodes } from "../../../shared/errors/InfrastructureError.js";
// import { DomainError, DomainErrorCodes } from "../../../shared/errors/DomainError.js"; // Removed unused import
/**
* File system implementation of the JSON document repository
* Stores JSON documents as files in a directory structure
*/
export class FileSystemJsonDocumentRepository {
fileSystemService;
indexService;
rootPath;
/**
* Create a new FileSystemJsonDocumentRepository
* @param fileSystemService File system service for file operations
* @param indexService Document index service for efficient lookups
* @param rootPath Root path for storing documents
*/
constructor(fileSystemService, indexService, rootPath) {
this.fileSystemService = fileSystemService;
this.indexService = indexService;
this.rootPath = rootPath;
}
/**
* Get the absolute file path for a document
* @param branchInfo Branch information
* @param documentPath Document path
* @returns Absolute file path
*/
getAbsoluteFilePath(branchInfo, documentPath) {
// Normalize branch name for file system (replace / with -)
const normalizedBranchName = branchInfo.name.replace(/\//g, '-');
// Build path: rootPath / branch-name / document-path
return path.join(this.rootPath, normalizedBranchName, documentPath.value);
}
/**
* Get the directory path for a branch
* @param branchInfo Branch information
* @returns Absolute directory path
*/
getBranchDirectoryPath(branchInfo) {
// Normalize branch name for file system (replace / with -)
const normalizedBranchName = branchInfo.name.replace(/\//g, '-');
// Build path: rootPath / branch-name
return path.join(this.rootPath, normalizedBranchName);
}
/**
* Find a document by its ID
* @param id Document ID
* @returns Promise resolving to the document if found, or null if not found
*/
async findById(id) {
try {
// Find all branch directories
const files = await this.fileSystemService.listFiles(this.rootPath);
const branchDirs = new Set();
// Extract unique directories
for (const file of files) {
const relativePath = path.relative(this.rootPath, file);
const parts = relativePath.split(path.sep);
if (parts.length > 0) {
branchDirs.add(parts[0]);
}
}
// Look through each branch to find the document
for (const branchDir of branchDirs) {
const branchPath = path.join(this.rootPath, branchDir);
const isDir = await this.fileSystemService.directoryExists(branchPath);
if (!isDir) {
continue;
}
// Create branch info for this directory
const branchName = branchDir.replace(/-/g, '/'); // Convert back to original branch name
const branchInfo = BranchInfo.create(branchName);
// Find document by ID using index service
const docRef = await this.indexService.findById(branchInfo, id);
if (docRef) {
// Found the document, load it
const documentPath = DocumentPath.create(docRef.path);
const fullPath = this.getAbsoluteFilePath(branchInfo, documentPath);
const content = await this.fileSystemService.readFile(fullPath);
try {
return JsonDocument.fromString(content, documentPath);
}
catch (error) {
throw new InfrastructureError(InfrastructureErrorCodes.FILE_READ_ERROR, `Failed to parse document at ${fullPath}: ${error.message}`, { cause: error });
}
}
}
// Document not found in any branch
return null;
}
catch (error) {
if (error instanceof InfrastructureError) {
throw error;
}
throw new InfrastructureError(InfrastructureErrorCodes.PERSISTENCE_ERROR, `Failed to find document by ID: ${error.message}`, { cause: error });
}
}
/**
* Find a document by its path
* @param branchInfo Branch information
* @param path Document path
* @returns Promise resolving to the document if found, or null if not found
*/
async findByPath(branchInfo, path) {
try {
// First check if the document exists in the index
const docRef = await this.indexService.findByPath(branchInfo, path);
if (!docRef) {
// Document not found in index, try direct file access
const fullPath = this.getAbsoluteFilePath(branchInfo, path);
try {
// Check if file exists
const fileExists = await this.fileSystemService.fileExists(fullPath);
if (!fileExists) {
return null;
}
// File exists, read and parse it
const content = await this.fileSystemService.readFile(fullPath);
const document = JsonDocument.fromString(content, path);
// Add to index for future lookups
await this.indexService.addToIndex(branchInfo, document);
return document;
}
catch (error) {
// File doesn't exist or other error
return null;
}
}
else {
// Document found in index, load from file
const fullPath = this.getAbsoluteFilePath(branchInfo, path);
try {
const content = await this.fileSystemService.readFile(fullPath);
return JsonDocument.fromString(content, path);
}
catch (error) {
// File doesn't exist or other error
// Remove from index since file is missing
await this.indexService.removeFromIndex(branchInfo, path);
return null;
}
}
}
catch (error) {
if (error instanceof InfrastructureError) {
throw error;
}
throw new InfrastructureError(InfrastructureErrorCodes.PERSISTENCE_ERROR, `Failed to find document by path: ${error.message}`, { cause: error });
}
}
/**
* Find documents by tags
* @param branchInfo Branch information
* @param tags Tags to search for
* @param matchAll If true, documents must have all tags; if false, any tag is sufficient
* @returns Promise resolving to array of matching documents
*/
async findByTags(branchInfo, tags, matchAll = false) {
try {
// Use index service to find documents by tags
// findByTags の呼び出し方を修正
const docRefs = await this.indexService.findByTags({ branchInfo, tags, matchAll });
// Load all matching documents
const documents = [];
for (const docRef of docRefs) {
const docPath = DocumentPath.create(docRef.path);
const fullPath = this.getAbsoluteFilePath(branchInfo, docPath);
try {
const content = await this.fileSystemService.readFile(fullPath);
const document = JsonDocument.fromString(content, docPath);
documents.push(document);
}
catch (error) {
// Skip documents that can't be loaded
// This might happen if a document is deleted but still in the index
// We should remove it from the index
const docId = DocumentId.create(docRef.id);
await this.indexService.removeFromIndex(branchInfo, docId);
}
}
return documents;
}
catch (error) {
if (error instanceof InfrastructureError) {
throw error;
}
throw new InfrastructureError(InfrastructureErrorCodes.PERSISTENCE_ERROR, `Failed to find documents by tags: ${error.message}`, { cause: error });
}
}
/**
* Find documents by document type
* @param branchInfo Branch information
* @param documentType Document type to search for
* @returns Promise resolving to array of matching documents
*/
async findByType(branchInfo, documentType) {
try {
// Use index service to find documents by type
const docRefs = await this.indexService.findByType(branchInfo, documentType);
// Load all matching documents
const documents = [];
for (const docRef of docRefs) {
const docPath = DocumentPath.create(docRef.path);
const fullPath = this.getAbsoluteFilePath(branchInfo, docPath);
try {
const content = await this.fileSystemService.readFile(fullPath);
const document = JsonDocument.fromString(content, docPath);
documents.push(document);
}
catch (error) {
// Skip documents that can't be loaded
// This might happen if a document is deleted but still in the index
// We should remove it from the index
const docId = DocumentId.create(docRef.id);
await this.indexService.removeFromIndex(branchInfo, docId);
}
}
return documents;
}
catch (error) {
if (error instanceof InfrastructureError) {
throw error;
}
throw new InfrastructureError(InfrastructureErrorCodes.PERSISTENCE_ERROR, `Failed to find documents by type: ${error.message}`, { cause: error });
}
}
/**
* Save a document
* @param branchInfo Branch information
* @param document Document to save
* @returns Promise resolving to the saved document
*/
async save(branchInfo, document) {
try {
// Get the full file path
const fullPath = this.getAbsoluteFilePath(branchInfo, document.path);
// Ensure directory exists
const directory = path.dirname(fullPath);
await this.fileSystemService.createDirectory(directory);
// Serialize document to JSON
const content = document.toString(true); // Pretty print
// Write to file
await this.fileSystemService.writeFile(fullPath, content);
// Update index
await this.indexService.addToIndex(branchInfo, document);
return document;
}
catch (error) {
if (error instanceof InfrastructureError) {
throw error;
}
throw new InfrastructureError(InfrastructureErrorCodes.FILE_WRITE_ERROR, `Failed to save document: ${error.message}`, { cause: error });
}
}
/**
* Delete a document
* @param branchInfo Branch information
* @param document Document to delete (or document ID or path)
* @returns Promise resolving to boolean indicating success
*/
async delete(branchInfo, document) {
try {
let filePath;
// Determine file path based on input type
if (document instanceof JsonDocument) {
filePath = this.getAbsoluteFilePath(branchInfo, document.path);
}
else if (document instanceof DocumentPath) {
filePath = this.getAbsoluteFilePath(branchInfo, document);
}
else if (document instanceof DocumentId) {
// Find document by ID using index
const docRef = await this.indexService.findById(branchInfo, document);
if (!docRef) {
return false; // Document not found
}
const docPath = DocumentPath.create(docRef.path);
filePath = this.getAbsoluteFilePath(branchInfo, docPath);
}
else {
throw new InfrastructureError(InfrastructureErrorCodes.INVALID_ARGUMENT, 'Invalid document reference type');
}
// Remove from index
await this.indexService.removeFromIndex(branchInfo, document);
// Delete file
return await this.fileSystemService.deleteFile(filePath);
}
catch (error) {
if (error instanceof InfrastructureError) {
throw error;
}
throw new InfrastructureError(InfrastructureErrorCodes.FILE_SYSTEM_ERROR, `Failed to delete document: ${error.message}`, { cause: error });
}
}
/**
* List all documents
* @param branchInfo Branch information
* @returns Promise resolving to array of all documents
*/
async listAll(branchInfo) {
try {
// First try to use index
try {
const docRefs = await this.indexService.listAll(branchInfo);
// Load all documents
const documents = [];
const missingDocs = [];
for (const docRef of docRefs) {
const docPath = DocumentPath.create(docRef.path);
const fullPath = this.getAbsoluteFilePath(branchInfo, docPath);
try {
const content = await this.fileSystemService.readFile(fullPath);
const document = JsonDocument.fromString(content, docPath);
documents.push(document);
}
catch (error) {
// Skip documents that can't be loaded
// This might happen if a document is deleted but still in the index
// We should collect these for removal from index
missingDocs.push(docPath);
}
}
// Remove missing documents from index
for (const docPath of missingDocs) {
await this.indexService.removeFromIndex(branchInfo, docPath);
}
return documents;
}
catch (error) {
// If index fails, fall back to file system scanning
return await this.listAllFromFileSystem(branchInfo);
}
}
catch (error) {
if (error instanceof InfrastructureError) {
throw error;
}
throw new InfrastructureError(InfrastructureErrorCodes.FILE_SYSTEM_ERROR, `Failed to list all documents: ${error.message}`, { cause: error });
}
}
/**
* List all documents by scanning the file system
* @param branchInfo Branch information
* @returns Promise resolving to array of all documents
*/
async listAllFromFileSystem(branchInfo) {
// Get branch directory
const branchDir = this.getBranchDirectoryPath(branchInfo);
// Ensure branch directory exists
const dirExists = await this.fileSystemService.directoryExists(branchDir);
if (!dirExists) {
return [];
}
// Get all files in branch directory
const files = await this.fileSystemService.listFiles(branchDir);
// Filter JSON files
const jsonFiles = files.filter((file) => file.endsWith('.json'));
// Parse documents
const documents = [];
const jsonDocuments = []; // For rebuilding index
for (const filePath of jsonFiles) {
try {
// Get relative path from branch directory
const relativePath = path.relative(branchDir, filePath);
const documentPath = DocumentPath.create(relativePath);
// Read and parse document
const content = await this.fileSystemService.readFile(filePath);
const document = JsonDocument.fromString(content, documentPath);
documents.push(document);
jsonDocuments.push(document);
}
catch (error) {
// Skip invalid documents
// This could happen if a file has invalid JSON or doesn't match the expected schema
}
}
// Rebuild index with found documents
await this.indexService.buildIndex(branchInfo, jsonDocuments); // TODO: Fix type assertion
return documents;
}
/**
* Check if a document exists by path
* @param branchInfo Branch information
* @param path Document path
* @returns Promise resolving to boolean indicating existence
*/
async exists(branchInfo, path) {
try {
// First check index
const docRef = await this.indexService.findByPath(branchInfo, path);
if (docRef) {
// Verify file actually exists
const fullPath = this.getAbsoluteFilePath(branchInfo, path);
const fileExists = await this.fileSystemService.fileExists(fullPath);
if (!fileExists) {
// File doesn't exist, remove from index
await this.indexService.removeFromIndex(branchInfo, path);
return false;
}
return true;
}
// Not in index, check file system directly
const fullPath = this.getAbsoluteFilePath(branchInfo, path);
return await this.fileSystemService.fileExists(fullPath);
}
catch (error) {
if (error instanceof InfrastructureError) {
throw error;
}
throw new InfrastructureError(InfrastructureErrorCodes.FILE_SYSTEM_ERROR, `Failed to check if document exists: ${error.message}`, { cause: error });
}
}
}
//# sourceMappingURL=FileSystemJsonDocumentRepository.js.map