mirror of
https://github.com/heroblackink/ultimate-todoist-sync-for-obsidian.git
synced 2026-07-22 07:40:27 +00:00
feat: add comprehensive checkDatabase function with issue detection
This commit is contained in:
parent
79600b2122
commit
02a460428c
3 changed files with 257 additions and 153 deletions
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
169
src/settings.ts
169
src/settings.ts
|
|
@ -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}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue