feat: add comprehensive checkDatabase function with issue detection

This commit is contained in:
HeroBlackInk 2026-02-16 04:59:09 +08:00
parent 79600b2122
commit 02a460428c
3 changed files with 257 additions and 153 deletions

View file

@ -1,12 +1,34 @@
import { App} from 'obsidian';
import UltimateTodoistSyncForObsidian from "../main";
import { LogAction } from './logOperation';
import { TaskConflict, ConflictResolutionModal } from './conflictModal';
interface Due {
date?: string;
[key: string]: any; // allow for additional properties
}
[key: string]: any;
}
export interface DatabaseCheckIssue {
type: 'missing_file' | 'missing_metadata' | 'orphaned_task' | 'duplicate_task' | 'invalid_task_id' | 'content_mismatch' | 'status_mismatch' | 'empty_metadata';
filePath?: string;
taskId?: string;
details: string;
}
export interface DatabaseCheckResult {
success: boolean;
totalIssues: number;
issues: DatabaseCheckIssue[];
summary: {
missingFiles: number;
missingMetadata: number;
orphanedTasks: number;
duplicateTasks: number;
invalidTaskIds: number;
contentMismatches: number;
statusMismatches: number;
emptyMetadata: number;
};
}
export class CacheOperation {
app:App;
@ -138,15 +160,15 @@ export class CacheOperation {
}
getDefaultProjectNameForFilepath(filepath:string){
const metadatas = this.plugin.settings.fileMetadata
// Helper function to get all file metadatas
getDefaultProjectNameForFilepath(filepath: string): string {
const metadatas = this.plugin.settings.fileMetadata;
if (!metadatas[filepath] || metadatas[filepath].defaultProjectId === undefined) {
return this.plugin.settings.defaultProjectName
}
else{
const defaultProjectId = metadatas[filepath].defaultProjectId
const defaultProjectName = this.getProjectNameByIdFromCache(defaultProjectId)
return defaultProjectName
return this.plugin.settings.defaultProjectName;
} else {
const defaultProjectId = metadatas[filepath].defaultProjectId;
const defaultProjectName = this.getProjectNameByIdFromCache(defaultProjectId);
return defaultProjectName;
}
}
@ -749,4 +771,199 @@ export class CacheOperation {
}
}
async checkDatabase(noticeCallback?: (message: string) => void): Promise<DatabaseCheckResult> {
const issues: DatabaseCheckIssue[] = [];
const summary = {
missingFiles: 0,
missingMetadata: 0,
orphanedTasks: 0,
duplicateTasks: 0,
invalidTaskIds: 0,
contentMismatches: 0,
statusMismatches: 0,
emptyMetadata: 0
};
if (noticeCallback) {
noticeCallback('Checking database integrity...');
}
try {
const fileMetadatas = this.plugin.settings.fileMetadata;
const tasks = this.plugin.settings.todoistTasksData.tasks;
const taskIdsInCache = new Set<string>();
const taskIdsInFiles = new Map<string, { filePath: string; lineNumber: number }>();
for (const [filePath, metadata] of Object.entries(fileMetadatas)) {
const file = this.app.vault.getAbstractFileByPath(filePath);
if (!file) {
issues.push({
type: 'missing_file',
filePath,
details: `File "${filePath}" referenced in metadata does not exist`
});
summary.missingFiles++;
continue;
}
if (!metadata || !metadata.todoistTasks || metadata.todoistTasks.length === 0) {
issues.push({
type: 'empty_metadata',
filePath,
details: `Metadata for "${filePath}" is empty`
});
summary.emptyMetadata++;
continue;
}
const content = await this.app.vault.cachedRead(file);
const lines = content.split('\n');
for (const taskId of metadata.todoistTasks) {
if (taskIdsInFiles.has(taskId)) {
const existing = taskIdsInFiles.get(taskId)!;
issues.push({
type: 'duplicate_task',
filePath,
taskId,
details: `Task ${taskId} appears in multiple files: "${existing.filePath}" and "${filePath}"`
});
summary.duplicateTasks++;
}
taskIdsInFiles.set(taskId, { filePath, lineNumber: -1 });
const lineWithTask = lines.findIndex(line => line.includes(`%%[todoist_id:: ${taskId}]%%`) || line.includes(`%%[todoist_id::${taskId}]%%`));
if (lineWithTask === -1) {
issues.push({
type: 'missing_metadata',
filePath,
taskId,
details: `Task ${taskId} in metadata but not found in file "${filePath}"`
});
summary.missingMetadata++;
} else {
taskIdsInFiles.set(taskId, { filePath, lineNumber: lineWithTask });
}
}
}
for (const task of tasks) {
const taskId = task.id;
taskIdsInCache.add(taskId);
if (!taskIdsInFiles.has(taskId)) {
issues.push({
type: 'orphaned_task',
taskId,
details: `Task ${taskId} exists in cache but not in any file metadata`
});
summary.orphanedTasks++;
}
if (!task.path) {
issues.push({
type: 'missing_metadata',
taskId,
details: `Task ${taskId} in cache has no path information`
});
summary.missingMetadata++;
continue;
}
const file = this.app.vault.getAbstractFileByPath(task.path);
if (!file) {
issues.push({
type: 'missing_file',
filePath: task.path,
taskId,
details: `Task ${taskId} references missing file "${task.path}"`
});
summary.missingFiles++;
continue;
}
try {
const todoistTask = await this.plugin.todoistRestAPI.getTaskById(taskId);
if (!todoistTask) {
issues.push({
type: 'invalid_task_id',
taskId,
details: `Task ${taskId} not found in Todoist`
});
summary.invalidTaskIds++;
continue;
}
const fileContent = await this.app.vault.cachedRead(file);
const lines = fileContent.split('\n');
const fileTaskInfo = taskIdsInFiles.get(taskId);
if (fileTaskInfo && fileTaskInfo.lineNumber >= 0 && lines[fileTaskInfo.lineNumber]) {
const line = lines[fileTaskInfo.lineNumber];
const obsidianContent = this.extractTaskContent(line);
const todoistContent = todoistTask.content || '';
if (obsidianContent.trim() !== todoistContent.trim()) {
issues.push({
type: 'content_mismatch',
filePath: task.path,
taskId,
details: `Task ${taskId} content differs between Obsidian and Todoist`
});
summary.contentMismatches++;
}
const obsidianCompleted = /\[x\]/i.test(line);
const todoistCompleted = todoistTask.isCompleted || false;
if (obsidianCompleted !== todoistCompleted) {
issues.push({
type: 'status_mismatch',
filePath: task.path,
taskId,
details: `Task ${taskId} completion status differs between Obsidian (${obsidianCompleted}) and Todoist (${todoistCompleted})`
});
summary.statusMismatches++;
}
}
} catch (error) {
issues.push({
type: 'invalid_task_id',
taskId,
details: `Failed to verify task ${taskId} in Todoist: ${(error as Error).message}`
});
summary.invalidTaskIds++;
}
if (noticeCallback && taskIdsInCache.size % 10 === 0) {
noticeCallback(`Checked ${taskIdsInCache.size}/${tasks.length} tasks...`);
}
}
const totalIssues = Object.values(summary).reduce((a, b) => a + b, 0);
this.plugin.logOperation?.log('DATABASE_CHECKED', `Database check completed: ${totalIssues} issues found`);
return {
success: totalIssues === 0,
totalIssues,
issues,
summary
};
} catch (error) {
this.plugin.logOperation?.log('DATABASE_CHECK', `Database check failed: ${(error as Error).message}`);
return {
success: false,
totalIssues: 0,
issues: [{
type: 'missing_metadata',
details: `Database check failed: ${(error as Error).message}`
}],
summary
};
}
}
}

View file

@ -39,6 +39,8 @@ export type LogAction =
| 'CACHE_FILE_METADATA_DELETED'
| 'CACHE_RENAMED'
| 'CACHE_REBUILT'
| 'DATABASE_CHECK'
| 'DATABASE_CHECKED'
// Todoist API operations
| 'TODOIST_TASK_CREATED'
| 'TODOIST_TASK_UPDATED'

View file

@ -254,160 +254,45 @@ export class UltimateTodoistSyncSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Check Database')
.setDesc('Check for possible issues: sync error, file renaming not updated, or missed tasks not synchronized.')
.setDesc('Check for possible issues: sync errors, file renaming, missing tasks, or conflicts between Obsidian and Todoist.')
.addButton(button => button
.setButtonText('Check Database')
.onClick(async () => {
// Add code here to handle exporting Todoist data
if(!this.plugin.settings.apiInitialized){
new Notice(`Please set the todoist api first`)
return
}
//reinstall plugin
const checkNotice = new Notice('Checking database integrity...', 0);
//check file metadata
console.log('checking file metadata')
await this.plugin.cacheOperation.checkFileMetadata()
this.plugin.saveSettings()
const metadatas = await this.plugin.cacheOperation.getFileMetadatas()
// check default project task amounts
try{
const projectId = this.plugin.settings.defaultProjectId
let options = {}
options.projectId = projectId
const tasks = await this.plugin.todoistRestAPI.GetActiveTasks(options)
let length = tasks.length
if(length >= 300){
new Notice(`The number of tasks in the default project exceeds 300, reaching the upper limit. It is not possible to add more tasks. Please modify the default project.`)
}
//console.log(tasks)
}catch(error){
console.error(`An error occurred while get tasks from todoist: ${error.message}`);
}
if (!await this.plugin.checkAndHandleSyncLock()) return;
console.log('checking deleted tasks')
//check empty task
for (const key in metadatas) {
const value = metadatas[key];
//console.log(value)
for(const taskId of value.todoistTasks) {
//console.log(`${taskId}`)
let taskObject
try{
taskObject = await this.plugin.cacheOperation.loadTaskFromCacheyID(taskId)
}catch(error){
console.error(`An error occurred while loading task cache: ${error.message}`);
}
if(!taskObject){
console.log(`The task data of the ${taskId} is empty.`)
//get from todoist
try {
taskObject = await this.plugin.todoistRestAPI.getTaskById(taskId);
} catch (error) {
if (error.message.includes('404')) {
// 处理404错误
console.log(`Task ${taskId} seems to not exist.`);
await this.plugin.cacheOperation.deleteTaskIdFromMetadata(key,taskId)
continue
} else {
// 处理其他错误
console.error(error);
continue
}
}
}
};
}
this.plugin.saveSettings()
console.log('checking renamed files')
try{
//check renamed files
for (const key in metadatas) {
const value = metadatas[key];
//console.log(value)
const newDescription = this.plugin.taskParser.getObsidianUrlFromFilepath(key)
for(const taskId of value.todoistTasks) {
//console.log(`${taskId}`)
let taskObject
try{
taskObject = await this.plugin.cacheOperation.loadTaskFromCacheyID(taskId)
}catch(error){
console.error(`An error occurred while loading task ${taskId} from cache: ${error.message}`);
console.log(taskObject)
}
if(!taskObject){
console.log(`Task ${taskId} seems to not exist.`)
continue
}
if(!taskObject?.description){
console.log(`The description of the task ${taskId} is empty.`)
}
const oldDescription = taskObject?.description ?? '';
if(newDescription != oldDescription){
console.log('Preparing to update description.')
console.log(oldDescription)
console.log(newDescription)
try{
//await this.plugin.todoistSync.updateTaskDescription(key)
}catch(error){
console.error(`An error occurred while updating task discription: ${error.message}`);
}
}
};
}
//check empty file metadata
//check calendar format
//check omitted tasks
console.log('checking unsynced tasks')
const files = this.app.vault.getFiles()
files.forEach(async (v, i) => {
if(v.extension == "md"){
try{
//console.log(`Scanning file ${v.path}`)
await this.plugin.fileOperation.addTodoistLinkToFile(v.path)
if(this.plugin.settings.enableFullVaultSync){
await this.plugin.fileOperation.addTodoistTagToFile(v.path)
}
}catch(error){
console.error(`An error occurred while check new tasks in the file: ${v.path}, ${error.message}`);
}
}
const result = await this.plugin.cacheOperation.checkDatabase((message: string) => {
checkNotice.setMessage(message);
});
this.plugin.syncLock = false
new Notice(`All files have been scanned.`)
}catch(error){
console.error(`An error occurred while scanning the vault.:${error}`)
this.plugin.syncLock = false
}
checkNotice.hide();
if(result.success){
new Notice(`Database check passed! No issues found.`);
}else{
let message = `Found ${result.totalIssues} issues:\n`;
message += `- ${result.summary.missingFiles} missing files\n`;
message += `- ${result.summary.missingMetadata} missing metadata\n`;
message += `- ${result.summary.orphanedTasks} orphaned tasks\n`;
message += `- ${result.summary.duplicateTasks} duplicate tasks\n`;
message += `- ${result.summary.invalidTaskIds} invalid task IDs\n`;
message += `- ${result.summary.contentMismatches} content mismatches\n`;
message += `- ${result.summary.statusMismatches} status mismatches\n`;
message += `- ${result.summary.emptyMetadata} empty metadata`;
new Notice(message, 8000);
this.plugin.logOperation?.log('DATABASE_CHECK', `Found ${result.totalIssues} database issues`);
}
}catch(error){
checkNotice.hide();
new Notice(`Database check failed: ${error.message}`);
}
})
);