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.
This commit is contained in:
@@ -25,6 +25,13 @@ export interface IVaultAdapter {
|
|||||||
|
|
||||||
// File creation
|
// File creation
|
||||||
create(path: string, data: string): Promise<TFile>;
|
create(path: string, data: string): Promise<TFile>;
|
||||||
|
|
||||||
|
// File modification
|
||||||
|
modify(file: TFile, data: string): Promise<void>;
|
||||||
|
|
||||||
|
// File deletion
|
||||||
|
delete(file: TAbstractFile): Promise<void>;
|
||||||
|
trash(file: TAbstractFile, system: boolean): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -38,4 +38,16 @@ export class VaultAdapter implements IVaultAdapter {
|
|||||||
async create(path: string, data: string): Promise<TFile> {
|
async create(path: string, data: string): Promise<TFile> {
|
||||||
return this.vault.create(path, data);
|
return this.vault.create(path, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async modify(file: TFile, data: string): Promise<void> {
|
||||||
|
await this.vault.modify(file, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(file: TAbstractFile): Promise<void> {
|
||||||
|
await this.vault.delete(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async trash(file: TAbstractFile, system: boolean): Promise<void> {
|
||||||
|
await this.vault.trash(file, system);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { App } from 'obsidian';
|
|||||||
import { Tool, CallToolResult } from '../types/mcp-types';
|
import { Tool, CallToolResult } from '../types/mcp-types';
|
||||||
import { NoteTools } from './note-tools';
|
import { NoteTools } from './note-tools';
|
||||||
import { VaultTools } from './vault-tools';
|
import { VaultTools } from './vault-tools';
|
||||||
|
import { createNoteTools } from './note-tools-factory';
|
||||||
import { createVaultTools } from './vault-tools-factory';
|
import { createVaultTools } from './vault-tools-factory';
|
||||||
import { NotificationManager } from '../ui/notifications';
|
import { NotificationManager } from '../ui/notifications';
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ export class ToolRegistry {
|
|||||||
private notificationManager: NotificationManager | null = null;
|
private notificationManager: NotificationManager | null = null;
|
||||||
|
|
||||||
constructor(app: App) {
|
constructor(app: App) {
|
||||||
this.noteTools = new NoteTools(app);
|
this.noteTools = createNoteTools(app);
|
||||||
this.vaultTools = createVaultTools(app);
|
this.vaultTools = createVaultTools(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
src/tools/note-tools-factory.ts
Normal file
15
src/tools/note-tools-factory.ts
Normal file
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { App, TFile } from 'obsidian';
|
import { App, TFile } from 'obsidian';
|
||||||
import {
|
import {
|
||||||
CallToolResult,
|
CallToolResult,
|
||||||
ParsedNote,
|
ParsedNote,
|
||||||
ExcalidrawMetadata,
|
ExcalidrawMetadata,
|
||||||
UpdateFrontmatterResult,
|
UpdateFrontmatterResult,
|
||||||
UpdateSectionsResult,
|
UpdateSectionsResult,
|
||||||
@@ -16,9 +16,14 @@ import { ErrorMessages } from '../utils/error-messages';
|
|||||||
import { FrontmatterUtils } from '../utils/frontmatter-utils';
|
import { FrontmatterUtils } from '../utils/frontmatter-utils';
|
||||||
import { WaypointUtils } from '../utils/waypoint-utils';
|
import { WaypointUtils } from '../utils/waypoint-utils';
|
||||||
import { VersionUtils } from '../utils/version-utils';
|
import { VersionUtils } from '../utils/version-utils';
|
||||||
|
import { IVaultAdapter, IFileManagerAdapter } from '../adapters/interfaces';
|
||||||
|
|
||||||
export class NoteTools {
|
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(
|
async readNote(
|
||||||
path: string,
|
path: string,
|
||||||
@@ -67,7 +72,7 @@ export class NoteTools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await this.app.vault.read(file);
|
const content = await this.vault.read(file);
|
||||||
|
|
||||||
// If no special options, return simple content
|
// If no special options, return simple content
|
||||||
if (!parseFrontmatter) {
|
if (!parseFrontmatter) {
|
||||||
@@ -145,7 +150,7 @@ export class NoteTools {
|
|||||||
// Delete existing file before creating
|
// Delete existing file before creating
|
||||||
const existingFile = PathUtils.resolveFile(this.app, normalizedPath);
|
const existingFile = PathUtils.resolveFile(this.app, normalizedPath);
|
||||||
if (existingFile) {
|
if (existingFile) {
|
||||||
await this.app.vault.delete(existingFile);
|
await this.vault.delete(existingFile);
|
||||||
}
|
}
|
||||||
} else if (onConflict === 'rename') {
|
} else if (onConflict === 'rename') {
|
||||||
// Generate a unique name
|
// Generate a unique name
|
||||||
@@ -198,7 +203,7 @@ export class NoteTools {
|
|||||||
|
|
||||||
// Proceed with file creation
|
// Proceed with file creation
|
||||||
try {
|
try {
|
||||||
const file = await this.app.vault.create(finalPath, content);
|
const file = await this.vault.create(finalPath, content);
|
||||||
|
|
||||||
const result: CreateNoteResult = {
|
const result: CreateNoteResult = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -252,7 +257,7 @@ export class NoteTools {
|
|||||||
|
|
||||||
// Create the current folder if it doesn't exist
|
// Create the current folder if it doesn't exist
|
||||||
if (!PathUtils.pathExists(this.app, path)) {
|
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 {
|
try {
|
||||||
// Check for waypoint edit protection
|
// 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);
|
const waypointCheck = WaypointUtils.wouldAffectWaypoint(currentContent, content);
|
||||||
|
|
||||||
if (waypointCheck.affected) {
|
if (waypointCheck.affected) {
|
||||||
@@ -313,7 +318,7 @@ export class NoteTools {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.app.vault.modify(file, content);
|
await this.vault.modify(file, content);
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Note updated successfully: ${file.path}` }]
|
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)
|
// Use Obsidian's FileManager to rename (automatically updates links)
|
||||||
// Note: Obsidian's renameFile automatically updates all wikilinks
|
// 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
|
// Get the renamed file to get version info
|
||||||
const renamedFile = PathUtils.resolveFile(this.app, normalizedNewPath);
|
const renamedFile = PathUtils.resolveFile(this.app, normalizedNewPath);
|
||||||
@@ -524,11 +529,11 @@ export class NoteTools {
|
|||||||
// Perform actual deletion
|
// Perform actual deletion
|
||||||
if (soft) {
|
if (soft) {
|
||||||
// Move to trash using Obsidian's trash method
|
// 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}`;
|
destination = `.trash/${file.name}`;
|
||||||
} else {
|
} else {
|
||||||
// Permanent deletion
|
// Permanent deletion
|
||||||
await this.app.vault.delete(file);
|
await this.vault.delete(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: DeleteNoteResult = {
|
const result: DeleteNoteResult = {
|
||||||
@@ -595,7 +600,7 @@ export class NoteTools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await this.app.vault.read(file);
|
const content = await this.vault.read(file);
|
||||||
|
|
||||||
// Parse Excalidraw metadata (gracefully handles malformed files)
|
// Parse Excalidraw metadata (gracefully handles malformed files)
|
||||||
const metadata = FrontmatterUtils.parseExcalidrawMetadata(content);
|
const metadata = FrontmatterUtils.parseExcalidrawMetadata(content);
|
||||||
@@ -725,7 +730,7 @@ export class NoteTools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read current content
|
// Read current content
|
||||||
const content = await this.app.vault.read(file);
|
const content = await this.vault.read(file);
|
||||||
const extracted = FrontmatterUtils.extractFrontmatter(content);
|
const extracted = FrontmatterUtils.extractFrontmatter(content);
|
||||||
|
|
||||||
// Get current frontmatter or create new
|
// Get current frontmatter or create new
|
||||||
@@ -767,7 +772,7 @@ export class NoteTools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write back
|
// Write back
|
||||||
await this.app.vault.modify(file, newContent);
|
await this.vault.modify(file, newContent);
|
||||||
|
|
||||||
// Generate response with version info
|
// Generate response with version info
|
||||||
const result: UpdateFrontmatterResult = {
|
const result: UpdateFrontmatterResult = {
|
||||||
@@ -851,7 +856,7 @@ export class NoteTools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read current content
|
// Read current content
|
||||||
const content = await this.app.vault.read(file);
|
const content = await this.vault.read(file);
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
|
|
||||||
// Sort edits by startLine in descending order to apply from bottom to top
|
// 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');
|
const newContent = lines.join('\n');
|
||||||
|
|
||||||
// Write back
|
// Write back
|
||||||
await this.app.vault.modify(file, newContent);
|
await this.vault.modify(file, newContent);
|
||||||
|
|
||||||
// Generate response with version info
|
// Generate response with version info
|
||||||
const result: UpdateSectionsResult = {
|
const result: UpdateSectionsResult = {
|
||||||
|
|||||||
Reference in New Issue
Block a user