From 0185ca7d0007d374e96870c46e2cccf6f69fbc08 Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 19 Oct 2025 23:53:48 -0400 Subject: [PATCH] refactor: migrate NoteTools to use adapter pattern Implements Task 10 from the implementation plan: 1. Updated NoteTools constructor to accept IVaultAdapter and IFileManagerAdapter 2. Created note-tools-factory.ts with factory function 3. Migrated all CRUD methods to use adapters: - readNote: uses vault.read() - createNote: uses vault.create(), vault.delete() - createParentFolders: uses vault.createFolder() - updateNote: uses vault.read(), vault.modify() - deleteNote: uses vault.trash(), vault.delete() - renameFile: uses fileManager.renameFile() - readExcalidraw: uses vault.read() - updateFrontmatter: uses vault.read(), vault.modify() - updateSections: uses vault.read(), vault.modify() 4. Extended IVaultAdapter interface with modify(), delete(), and trash() methods 5. Implemented new methods in VaultAdapter 6. Updated ToolRegistry to use factory function 7. Kept app parameter temporarily for PathUtils dependency 8. All methods now use adapters instead of direct Obsidian API calls 9. Code compiles successfully This change enables 100% test coverage by allowing full mocking of vault operations. --- src/adapters/interfaces.ts | 7 ++++++ src/adapters/vault-adapter.ts | 12 ++++++++++ src/tools/index.ts | 3 ++- src/tools/note-tools-factory.ts | 15 ++++++++++++ src/tools/note-tools.ts | 41 ++++++++++++++++++--------------- 5 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 src/tools/note-tools-factory.ts diff --git a/src/adapters/interfaces.ts b/src/adapters/interfaces.ts index 3e5163a..cefd99c 100644 --- a/src/adapters/interfaces.ts +++ b/src/adapters/interfaces.ts @@ -25,6 +25,13 @@ export interface IVaultAdapter { // File creation create(path: string, data: string): Promise; + + // File modification + modify(file: TFile, data: string): Promise; + + // File deletion + delete(file: TAbstractFile): Promise; + trash(file: TAbstractFile, system: boolean): Promise; } /** diff --git a/src/adapters/vault-adapter.ts b/src/adapters/vault-adapter.ts index 18ee541..4bd61d6 100644 --- a/src/adapters/vault-adapter.ts +++ b/src/adapters/vault-adapter.ts @@ -38,4 +38,16 @@ export class VaultAdapter implements IVaultAdapter { async create(path: string, data: string): Promise { return this.vault.create(path, data); } + + async modify(file: TFile, data: string): Promise { + await this.vault.modify(file, data); + } + + async delete(file: TAbstractFile): Promise { + await this.vault.delete(file); + } + + async trash(file: TAbstractFile, system: boolean): Promise { + await this.vault.trash(file, system); + } } \ No newline at end of file diff --git a/src/tools/index.ts b/src/tools/index.ts index cc4d8bb..59a2a28 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -2,6 +2,7 @@ import { App } from 'obsidian'; import { Tool, CallToolResult } from '../types/mcp-types'; import { NoteTools } from './note-tools'; import { VaultTools } from './vault-tools'; +import { createNoteTools } from './note-tools-factory'; import { createVaultTools } from './vault-tools-factory'; import { NotificationManager } from '../ui/notifications'; @@ -11,7 +12,7 @@ export class ToolRegistry { private notificationManager: NotificationManager | null = null; constructor(app: App) { - this.noteTools = new NoteTools(app); + this.noteTools = createNoteTools(app); this.vaultTools = createVaultTools(app); } diff --git a/src/tools/note-tools-factory.ts b/src/tools/note-tools-factory.ts new file mode 100644 index 0000000..879968e --- /dev/null +++ b/src/tools/note-tools-factory.ts @@ -0,0 +1,15 @@ +import { App } from 'obsidian'; +import { NoteTools } from './note-tools'; +import { VaultAdapter } from '../adapters/vault-adapter'; +import { FileManagerAdapter } from '../adapters/file-manager-adapter'; + +/** + * Factory function to create NoteTools with concrete adapters + */ +export function createNoteTools(app: App): NoteTools { + return new NoteTools( + new VaultAdapter(app.vault), + new FileManagerAdapter(app.fileManager), + app + ); +} \ No newline at end of file diff --git a/src/tools/note-tools.ts b/src/tools/note-tools.ts index 81ae01d..8f37039 100644 --- a/src/tools/note-tools.ts +++ b/src/tools/note-tools.ts @@ -1,7 +1,7 @@ import { App, TFile } from 'obsidian'; -import { - CallToolResult, - ParsedNote, +import { + CallToolResult, + ParsedNote, ExcalidrawMetadata, UpdateFrontmatterResult, UpdateSectionsResult, @@ -16,9 +16,14 @@ import { ErrorMessages } from '../utils/error-messages'; import { FrontmatterUtils } from '../utils/frontmatter-utils'; import { WaypointUtils } from '../utils/waypoint-utils'; import { VersionUtils } from '../utils/version-utils'; +import { IVaultAdapter, IFileManagerAdapter } from '../adapters/interfaces'; export class NoteTools { - constructor(private app: App) {} + constructor( + private vault: IVaultAdapter, + private fileManager: IFileManagerAdapter, + private app: App // Keep temporarily for methods not yet migrated + ) {} async readNote( path: string, @@ -67,7 +72,7 @@ export class NoteTools { } try { - const content = await this.app.vault.read(file); + const content = await this.vault.read(file); // If no special options, return simple content if (!parseFrontmatter) { @@ -145,7 +150,7 @@ export class NoteTools { // Delete existing file before creating const existingFile = PathUtils.resolveFile(this.app, normalizedPath); if (existingFile) { - await this.app.vault.delete(existingFile); + await this.vault.delete(existingFile); } } else if (onConflict === 'rename') { // Generate a unique name @@ -198,7 +203,7 @@ export class NoteTools { // Proceed with file creation try { - const file = await this.app.vault.create(finalPath, content); + const file = await this.vault.create(finalPath, content); const result: CreateNoteResult = { success: true, @@ -252,7 +257,7 @@ export class NoteTools { // Create the current folder if it doesn't exist if (!PathUtils.pathExists(this.app, path)) { - await this.app.vault.createFolder(path); + await this.vault.createFolder(path); } } @@ -292,7 +297,7 @@ export class NoteTools { try { // Check for waypoint edit protection - const currentContent = await this.app.vault.read(file); + const currentContent = await this.vault.read(file); const waypointCheck = WaypointUtils.wouldAffectWaypoint(currentContent, content); if (waypointCheck.affected) { @@ -313,7 +318,7 @@ export class NoteTools { }; } - await this.app.vault.modify(file, content); + await this.vault.modify(file, content); return { content: [{ type: "text", text: `Note updated successfully: ${file.path}` }] }; @@ -424,7 +429,7 @@ export class NoteTools { // Use Obsidian's FileManager to rename (automatically updates links) // Note: Obsidian's renameFile automatically updates all wikilinks - await this.app.fileManager.renameFile(file, normalizedNewPath); + await this.fileManager.renameFile(file, normalizedNewPath); // Get the renamed file to get version info const renamedFile = PathUtils.resolveFile(this.app, normalizedNewPath); @@ -524,11 +529,11 @@ export class NoteTools { // Perform actual deletion if (soft) { // Move to trash using Obsidian's trash method - await this.app.vault.trash(file, true); + await this.vault.trash(file, true); destination = `.trash/${file.name}`; } else { // Permanent deletion - await this.app.vault.delete(file); + await this.vault.delete(file); } const result: DeleteNoteResult = { @@ -595,7 +600,7 @@ export class NoteTools { } try { - const content = await this.app.vault.read(file); + const content = await this.vault.read(file); // Parse Excalidraw metadata (gracefully handles malformed files) const metadata = FrontmatterUtils.parseExcalidrawMetadata(content); @@ -725,7 +730,7 @@ export class NoteTools { } // Read current content - const content = await this.app.vault.read(file); + const content = await this.vault.read(file); const extracted = FrontmatterUtils.extractFrontmatter(content); // Get current frontmatter or create new @@ -767,7 +772,7 @@ export class NoteTools { } // Write back - await this.app.vault.modify(file, newContent); + await this.vault.modify(file, newContent); // Generate response with version info const result: UpdateFrontmatterResult = { @@ -851,7 +856,7 @@ export class NoteTools { } // Read current content - const content = await this.app.vault.read(file); + const content = await this.vault.read(file); const lines = content.split('\n'); // Sort edits by startLine in descending order to apply from bottom to top @@ -891,7 +896,7 @@ export class NoteTools { const newContent = lines.join('\n'); // Write back - await this.app.vault.modify(file, newContent); + await this.vault.modify(file, newContent); // Generate response with version info const result: UpdateSectionsResult = {