logic.ts•7.15 kB
/**
 * @fileoverview Business logic for the Obsidian Dataview Query tool.
 * Handles execution of Dataview DQL queries via the Obsidian REST API.
 * @module obsidianDataviewQueryTool/logic
 */
import { z } from "zod";
import { logger, RequestContext } from "../../../utils/index.js";
import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
import { ComplexSearchResult } from "../../../services/obsidianRestAPI/types.js";
/**
 * Input validation schema for the Dataview query tool.
 */
export const DataviewQueryInputSchema = z.object({
  query: z
    .string()
    .min(1, "Query cannot be empty")
    .refine(
      (query) => {
        const trimmed = query.trim().toUpperCase();
        return trimmed.startsWith("TABLE") || 
               trimmed.startsWith("LIST") || 
               trimmed.startsWith("TASK") ||
               trimmed.startsWith("CALENDAR");
      },
      "Query must start with TABLE, LIST, TASK, or CALENDAR (Dataview DQL keywords)"
    ),
  format: z.enum(["table", "list", "raw"]).default("table"),
});
/**
 * Type definition for validated input.
 */
export type DataviewQueryInput = z.infer<typeof DataviewQueryInputSchema>;
/**
 * Interface for formatted query results.
 */
export interface DataviewQueryResponse {
  success: boolean;
  resultCount: number;
  query: string;
  format: string;
  data: string | object;
  executionTime?: string;
  error?: string;
}
/**
 * Formats Dataview query results based on the requested format.
 */
function formatQueryResults(
  results: ComplexSearchResult[],
  format: string,
  query: string
): DataviewQueryResponse {
  const resultCount = results.length;
  
  if (resultCount === 0) {
    return {
      success: true,
      resultCount: 0,
      query,
      format,
      data: format === "raw" ? [] : "No results found for the query.",
    };
  }
  switch (format) {
    case "raw":
      return {
        success: true,
        resultCount,
        query,
        format,
        data: results,
      };
    case "list":
      const listItems = results.map((result, index) => {
        const title = result.filename || `Result ${index + 1}`;
        // Note: ComplexSearchResult only has filename and result fields
        const content = result.result 
          ? (typeof result.result === "string" ? result.result.substring(0, 100) : JSON.stringify(result.result).substring(0, 100))
          : "";
        const suffix = content.length > 100 ? "..." : "";
        return `• **${title}**${content ? ` - ${content}${suffix}` : ""}`;
      });
      
      return {
        success: true,
        resultCount,
        query,
        format,
        data: `Query Results (${resultCount} items):\n\n${listItems.join("\n")}`,
      };
    case "table":
    default:
      // Create a formatted table view
      const tableHeader = "| File | Result |\n|------|--------|";
      const tableRows = results.map((result) => {
        const filename = result.filename || "Unknown";
        const resultData = result.result 
          ? (typeof result.result === "string" 
             ? result.result.substring(0, 80).replace(/\|/g, "\\|").replace(/\n/g, " ")
             : JSON.stringify(result.result).substring(0, 80).replace(/\|/g, "\\|").replace(/\n/g, " "))
          : "N/A";
        
        return `| ${filename} | ${resultData}${resultData.length >= 80 ? "..." : ""} |`;
      });
      const tableData = `Query Results (${resultCount} items):\n\n${tableHeader}\n${tableRows.join("\n")}`;
      
      return {
        success: true,
        resultCount,
        query,
        format,
        data: tableData,
      };
  }
}
/**
 * Executes a Dataview DQL query against the Obsidian vault.
 * 
 * @param obsidianService - The Obsidian REST API service instance
 * @param input - The validated input containing the query and format
 * @param context - Request context for logging and error handling
 * @returns Promise resolving to formatted query results
 */
export async function executeDataviewQuery(
  obsidianService: ObsidianRestApiService,
  input: DataviewQueryInput,
  context: RequestContext,
): Promise<DataviewQueryResponse> {
  const startTime = Date.now();
  
  try {
    logger.info("Executing Dataview DQL query", {
      ...context,
      operation: "dataviewQuery",
      queryLength: input.query.length,
      format: input.format,
      queryPreview: input.query.substring(0, 100),
    });
    // Execute the Dataview query using the existing searchComplex method
    const results = await obsidianService.searchComplex(
      input.query,
      "application/vnd.olrapi.dataview.dql+txt",
      context
    );
    const executionTime = `${Date.now() - startTime}ms`;
    
    logger.info("Dataview query executed successfully", {
      ...context,
      operation: "dataviewQuery",
      resultCount: results.length,
      executionTime,
    });
    // Format the results based on the requested format
    const formattedResult = formatQueryResults(results, input.format, input.query);
    formattedResult.executionTime = executionTime;
    return formattedResult;
  } catch (error) {
    const executionTime = `${Date.now() - startTime}ms`;
    
    // Handle specific Dataview-related errors
    if (error instanceof Error) {
      // Check for common Dataview errors
      if (error.message.includes("Dataview") || error.message.includes("DQL")) {
        logger.warning("Dataview query execution failed - likely plugin not installed or query syntax error", {
          ...context,
          operation: "dataviewQuery",
          error: error.message,
          executionTime,
        });
        
        return {
          success: false,
          resultCount: 0,
          query: input.query,
          format: input.format,
          data: "Dataview query failed. Please ensure:\n" +
                "1. The Dataview plugin is installed and enabled in Obsidian\n" +
                "2. Your query syntax is valid DQL\n" +
                "3. The query type (TABLE/LIST/TASK) matches your data\n\n" +
                `Error: ${error.message}`,
          executionTime,
          error: error.message,
        };
      }
    }
    // Re-throw for other types of errors (authentication, network, etc.)
    throw error;
  }
}
/**
 * Main logic function for the Obsidian Dataview Query tool.
 * Validates input, executes the query, and returns formatted results.
 * 
 * @param input - The input arguments from the MCP client
 * @param context - Request context for logging and error handling
 * @param obsidianService - The Obsidian REST API service instance
 * @returns Promise resolving to the tool execution result
 */
export async function obsidianDataviewQueryLogic(
  input: DataviewQueryInput,
  context: RequestContext,
  obsidianService: ObsidianRestApiService,
): Promise<DataviewQueryResponse> {
  logger.debug("Processing Dataview query input", {
    ...context,
    operation: "dataviewQuery",
    hasQuery: !!input.query,
    format: input.format,
  });
  // Execute the query
  const result = await executeDataviewQuery(obsidianService, input, context);
  
  return result;
}