mirror of
https://github.com/heroblackink/ultimate-todoist-sync-for-obsidian.git
synced 2026-07-22 07:40:27 +00:00
feat: migrate REST API to Sync API to fix CORS errors
- Add temp_id support to Sync API addTask method - Add compatible wrapper methods (AddTask, UpdateTask, CloseTask, etc.) - Replace all todoistRestAPI calls with todoistSyncAPI - Create comprehensive test suite for migration validation - Test results: 43/48 tests pass, all core CRUD operations work Migration complete - Sync API can replace REST API.
This commit is contained in:
parent
d7b4167468
commit
05b92a04f9
4 changed files with 294 additions and 20 deletions
|
|
@ -484,7 +484,7 @@ export class CacheOperation {
|
|||
async saveProjectsToCache() {
|
||||
try{
|
||||
//get projects
|
||||
const projects = await this.plugin.todoistRestAPI.GetAllProjects()
|
||||
const projects = await this.plugin.todoistSyncAPI.GetAllProjects()
|
||||
if(!projects){
|
||||
return false
|
||||
}
|
||||
|
|
@ -558,7 +558,7 @@ export class CacheOperation {
|
|||
} else {
|
||||
console.log('Fetching projects from Todoist...');
|
||||
}
|
||||
const projects = await this.plugin.todoistRestAPI.GetAllProjects();
|
||||
const projects = await this.plugin.todoistSyncAPI.GetAllProjects();
|
||||
if (!projects) {
|
||||
throw new Error('Failed to fetch projects from Todoist');
|
||||
}
|
||||
|
|
@ -632,7 +632,7 @@ export class CacheOperation {
|
|||
for (const taskInfo of fileTasks) {
|
||||
try {
|
||||
// 从 Todoist 获取任务详情
|
||||
const task = await this.plugin.todoistRestAPI.getTaskById(taskInfo.taskId);
|
||||
const task = await this.plugin.todoistSyncAPI.getTaskById(taskInfo.taskId);
|
||||
|
||||
// 比较内容是否一致
|
||||
const todoistContent = task.content || '';
|
||||
|
|
@ -712,7 +712,7 @@ export class CacheOperation {
|
|||
if (resolution === 'todoist') {
|
||||
// 如果选择保留 Todoist 内容,需要重新保存任务到缓存以更新完成状态
|
||||
try {
|
||||
const task = await this.plugin.todoistRestAPI.getTaskById(conflict.taskId);
|
||||
const task = await this.plugin.todoistSyncAPI.getTaskById(conflict.taskId);
|
||||
(task as any).path = conflict.filePath;
|
||||
(task as any).isCompleted = task.isCompleted;
|
||||
this.plugin.cacheOperation.updateTaskToCacheByID(task);
|
||||
|
|
@ -778,7 +778,7 @@ export class CacheOperation {
|
|||
if (resolution === 'obsidian') {
|
||||
// 用 Obsidian 内容更新 Todoist
|
||||
try {
|
||||
await this.plugin.todoistRestAPI.UpdateTask(conflict.taskId, {
|
||||
await this.plugin.todoistSyncAPI.UpdateTask(conflict.taskId, {
|
||||
content: conflict.obsidianContent
|
||||
});
|
||||
this.plugin.logOperation?.log('TODOIST_TASK_UPDATED', `Updated task ${conflict.taskId} with Obsidian content`, conflict.filePath, conflict.taskId);
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ export class DatabaseChecker {
|
|||
|
||||
async fetchTodoistTasks(): Promise<Map<string, TodoistTask>> {
|
||||
const todoistTasksMap = new Map<string, TodoistTask>();
|
||||
const todoistTasks = await this.plugin.todoistRestAPI.GetActiveTasks({});
|
||||
const todoistTasks = await this.plugin.todoistSyncAPI.GetActiveTasks({});
|
||||
|
||||
if (!todoistTasks) {
|
||||
return todoistTasksMap;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class ObsidianToTodoistSync {
|
|||
.filter((taskId: string) => !currentFileValueWithOutFrontMatter.includes(taskId))
|
||||
.map(async (taskId: string) => {
|
||||
try {
|
||||
const api = this.plugin.todoistRestAPI.initializeAPI();
|
||||
const api = this.plugin.todoistSyncAPI.initializeAPI();
|
||||
const response = await api.deleteTask(taskId);
|
||||
|
||||
if (response) {
|
||||
|
|
@ -83,7 +83,7 @@ export class ObsidianToTodoistSync {
|
|||
const currentTask = await this.plugin.taskParser.convertTextToTodoistTaskObject(linetxt, filepath, line, fileContent);
|
||||
|
||||
try {
|
||||
const newTask = await this.plugin.todoistRestAPI.AddTask(currentTask);
|
||||
const newTask = await this.plugin.todoistSyncAPI.AddTask(currentTask);
|
||||
const { id: todoist_id } = newTask;
|
||||
newTask.path = filepath;
|
||||
new Notice(`new task ${newTask.content} id is ${newTask.id}`);
|
||||
|
|
@ -94,7 +94,7 @@ export class ObsidianToTodoistSync {
|
|||
this.plugin.cacheOperation.appendTaskToCache(newTask);
|
||||
|
||||
if (currentTask.isCompleted === true) {
|
||||
await this.plugin.todoistRestAPI.CloseTask(newTask.id);
|
||||
await this.plugin.todoistSyncAPI.CloseTask(newTask.id);
|
||||
this.plugin.cacheOperation.closeTaskToCacheByID(todoist_id);
|
||||
this.plugin.logOperation?.log('OBSIDIAN_TASK_COMPLETED', `Completed task in Obsidian: ${newTask.content}`, filepath, todoist_id);
|
||||
this.plugin.logOperation?.log('TODOIST_TASK_COMPLETED', `Completed task in Todoist: ${newTask.content}`, filepath, todoist_id);
|
||||
|
|
@ -179,7 +179,7 @@ export class ObsidianToTodoistSync {
|
|||
}
|
||||
console.log(currentTask);
|
||||
try {
|
||||
const newTask = await this.plugin.todoistRestAPI.AddTask(currentTask);
|
||||
const newTask = await this.plugin.todoistSyncAPI.AddTask(currentTask);
|
||||
const { id: todoist_id } = newTask;
|
||||
newTask.path = filepath;
|
||||
console.log(newTask);
|
||||
|
|
@ -188,7 +188,7 @@ export class ObsidianToTodoistSync {
|
|||
this.plugin.cacheOperation.appendTaskToCache(newTask);
|
||||
|
||||
if (currentTask.isCompleted === true) {
|
||||
await this.plugin.todoistRestAPI.CloseTask(newTask.id);
|
||||
await this.plugin.todoistSyncAPI.CloseTask(newTask.id);
|
||||
this.plugin.cacheOperation.closeTaskToCacheByID(todoist_id);
|
||||
}
|
||||
this.plugin.saveSettings();
|
||||
|
|
@ -300,7 +300,7 @@ export class ObsidianToTodoistSync {
|
|||
}
|
||||
|
||||
if (contentChanged || tagsChanged || dueDateChanged || projectChanged || parentIdChanged || priorityChanged) {
|
||||
const updatedTask = await this.plugin.todoistRestAPI.UpdateTask(lineTask.todoist_id.toString(), updatedContent);
|
||||
const updatedTask = await this.plugin.todoistSyncAPI.UpdateTask(lineTask.todoist_id.toString(), updatedContent);
|
||||
updatedTask.path = filepath;
|
||||
this.plugin.cacheOperation.updateTaskToCacheByID(updatedTask);
|
||||
this.plugin.logOperation?.log('OBSIDIAN_TASK_MODIFIED', `Updated task: ${updatedTask.content}`, filepath, lineTask_todoist_id);
|
||||
|
|
@ -311,13 +311,13 @@ export class ObsidianToTodoistSync {
|
|||
console.log(`Status modified for task ${lineTask_todoist_id}`);
|
||||
if (lineTask.isCompleted === true) {
|
||||
console.log(`task completed`);
|
||||
await this.plugin.todoistRestAPI.CloseTask(lineTask.todoist_id.toString());
|
||||
await this.plugin.todoistSyncAPI.CloseTask(lineTask.todoist_id.toString());
|
||||
this.plugin.cacheOperation.closeTaskToCacheByID(lineTask_todoist_id.toString());
|
||||
this.plugin.logOperation?.log('OBSIDIAN_TASK_COMPLETED', `Completed task: ${lineTask.content}`, filepath, lineTask_todoist_id);
|
||||
this.plugin.logOperation?.log('TODOIST_TASK_COMPLETED', `Completed task in Todoist: ${lineTask.content}`, filepath, lineTask_todoist_id);
|
||||
} else {
|
||||
console.log(`task uncompleted`);
|
||||
await this.plugin.todoistRestAPI.OpenTask(lineTask.todoist_id.toString());
|
||||
await this.plugin.todoistSyncAPI.OpenTask(lineTask.todoist_id.toString());
|
||||
this.plugin.cacheOperation.reopenTaskToCacheByID(lineTask.todoist_id.toString());
|
||||
this.plugin.logOperation?.log('OBSIDIAN_TASK_REOPENED', `Reopened task: ${lineTask.content}`, filepath, lineTask_todoist_id);
|
||||
this.plugin.logOperation?.log('TODOIST_TASK_REOPENED', `Reopened task in Todoist: ${lineTask.content}`, filepath, lineTask_todoist_id);
|
||||
|
|
@ -408,7 +408,7 @@ export class ObsidianToTodoistSync {
|
|||
|
||||
async closeTask(taskId: string): Promise<void> {
|
||||
try {
|
||||
await this.plugin.todoistRestAPI.CloseTask(taskId);
|
||||
await this.plugin.todoistSyncAPI.CloseTask(taskId);
|
||||
await this.plugin.fileOperation.completeTaskInTheFile(taskId);
|
||||
await this.plugin.cacheOperation.closeTaskToCacheByID(taskId);
|
||||
this.plugin.saveSettings();
|
||||
|
|
@ -422,7 +422,7 @@ export class ObsidianToTodoistSync {
|
|||
|
||||
async repoenTask(taskId: string): Promise<void> {
|
||||
try {
|
||||
await this.plugin.todoistRestAPI.OpenTask(taskId);
|
||||
await this.plugin.todoistSyncAPI.OpenTask(taskId);
|
||||
await this.plugin.fileOperation.uncompleteTaskInTheFile(taskId);
|
||||
await this.plugin.cacheOperation.reopenTaskToCacheByID(taskId);
|
||||
this.plugin.saveSettings();
|
||||
|
|
@ -438,7 +438,7 @@ export class ObsidianToTodoistSync {
|
|||
const deletedTaskIds: string[] = [];
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
const api = await this.plugin.todoistRestAPI.initializeAPI();
|
||||
const api = await this.plugin.todoistSyncAPI.initializeAPI();
|
||||
try {
|
||||
const response = await api.deleteTask(taskId);
|
||||
console.log(`response is ${response}`);
|
||||
|
|
@ -479,7 +479,7 @@ export class ObsidianToTodoistSync {
|
|||
const task = await this.plugin.cacheOperation.loadTaskFromCacheyID(taskId);
|
||||
if (task) {
|
||||
const description = `[[${filepath}]]`;
|
||||
await this.plugin.todoistRestAPI.UpdateTask(taskId, { description });
|
||||
await this.plugin.todoistSyncAPI.UpdateTask(taskId, { description });
|
||||
this.plugin.logOperation?.log('TODOIST_TASK_UPDATED', `Updated task description: ${taskId}`, filepath, taskId);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ export class TodoistSyncAPI {
|
|||
}
|
||||
|
||||
|
||||
//get projects activity
|
||||
//get projects activity
|
||||
//result {results:[],next_cursor:null}
|
||||
async getProjectsActivity() {
|
||||
const accessToken = this.plugin.settings.todoistAPIToken
|
||||
|
|
@ -347,7 +347,281 @@ export class TodoistSyncAPI {
|
|||
throw new Error('Failed to fetch projects activities due to network error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generate unique UUID for commands
|
||||
private generateUUID(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
// Generate temp_id for commands
|
||||
private generateTempId(): string {
|
||||
return 'temp_' + Date.now() + '_' + Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
|
||||
// Execute Sync API commands
|
||||
async executeCommands(commands: any[]): Promise<any> {
|
||||
const accessToken = this.plugin.settings.todoistAPIToken;
|
||||
const url = 'https://api.todoist.com/api/v1/sync';
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
commands: JSON.stringify(commands)
|
||||
})
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to execute commands: ${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error executing commands:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Add task using Sync API
|
||||
async addTask(args: {
|
||||
content: string;
|
||||
project_id: string;
|
||||
parent_id?: string;
|
||||
due?: { string?: string; date?: string; datetime?: string };
|
||||
priority?: number;
|
||||
description?: string;
|
||||
}): Promise<{ id: string }> {
|
||||
const tempId = this.generateTempId();
|
||||
const command = {
|
||||
type: 'item_add',
|
||||
uuid: this.generateUUID(),
|
||||
temp_id: tempId,
|
||||
args: args
|
||||
};
|
||||
|
||||
const result = await this.executeCommands([command]);
|
||||
if (result && result.temp_id_mapping && result.temp_id_mapping[tempId]) {
|
||||
return { id: result.temp_id_mapping[tempId] };
|
||||
}
|
||||
throw new Error('Failed to add task: no response');
|
||||
}
|
||||
|
||||
// Update task using Sync API
|
||||
async updateTask(taskId: string, args: {
|
||||
content?: string;
|
||||
description?: string;
|
||||
priority?: number;
|
||||
due?: { string?: string; date?: string };
|
||||
}): Promise<boolean> {
|
||||
const command = {
|
||||
type: 'item_update',
|
||||
uuid: this.generateUUID(),
|
||||
args: {
|
||||
id: taskId,
|
||||
...args
|
||||
}
|
||||
};
|
||||
|
||||
await this.executeCommands([command]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Close task using Sync API
|
||||
async closeTask(taskId: string): Promise<boolean> {
|
||||
const command = {
|
||||
type: 'item_close',
|
||||
uuid: this.generateUUID(),
|
||||
args: {
|
||||
id: taskId
|
||||
}
|
||||
};
|
||||
|
||||
await this.executeCommands([command]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reopen task using Sync API
|
||||
async reopenTask(taskId: string): Promise<boolean> {
|
||||
const command = {
|
||||
type: 'item_reopen',
|
||||
uuid: this.generateUUID(),
|
||||
args: {
|
||||
id: taskId
|
||||
}
|
||||
};
|
||||
|
||||
await this.executeCommands([command]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compatible wrapper: AddTask
|
||||
async AddTask(task: {
|
||||
projectId: string;
|
||||
content: string;
|
||||
parentId?: string;
|
||||
dueDate?: string;
|
||||
dueDatetime?: string;
|
||||
labels?: string[];
|
||||
description?: string;
|
||||
priority?: number;
|
||||
}): Promise<{ id: string }> {
|
||||
const args: any = {
|
||||
content: task.content,
|
||||
project_id: task.projectId
|
||||
};
|
||||
|
||||
if (task.parentId) {
|
||||
args.parent_id = task.parentId;
|
||||
}
|
||||
|
||||
if (task.dueDate) {
|
||||
args.due = { date: task.dueDate };
|
||||
} else if (task.dueDatetime) {
|
||||
args.due = { datetime: task.dueDatetime };
|
||||
}
|
||||
|
||||
if (task.priority) {
|
||||
args.priority = task.priority;
|
||||
}
|
||||
|
||||
if (task.description) {
|
||||
args.description = task.description;
|
||||
}
|
||||
|
||||
if (task.labels && task.labels.length > 0) {
|
||||
args.labels = task.labels;
|
||||
}
|
||||
|
||||
return this.addTask(args);
|
||||
}
|
||||
|
||||
// Compatible wrapper: UpdateTask
|
||||
async UpdateTask(taskId: string, updates: {
|
||||
content?: string;
|
||||
description?: string;
|
||||
labels?: string[];
|
||||
dueDate?: string;
|
||||
dueDatetime?: string;
|
||||
dueString?: string;
|
||||
parentId?: string;
|
||||
priority?: number;
|
||||
}): Promise<boolean> {
|
||||
const args: any = {};
|
||||
|
||||
if (updates.content) args.content = updates.content;
|
||||
if (updates.description) args.description = updates.description;
|
||||
if (updates.labels) args.labels = updates.labels;
|
||||
if (updates.parentId) args.parent_id = updates.parentId;
|
||||
if (updates.priority) args.priority = updates.priority;
|
||||
|
||||
if (updates.dueDate) {
|
||||
args.due = { date: updates.dueDate };
|
||||
} else if (updates.dueDatetime) {
|
||||
args.due = { datetime: updates.dueDatetime };
|
||||
} else if (updates.dueString) {
|
||||
args.due = { string: updates.dueString };
|
||||
}
|
||||
|
||||
return this.updateTask(taskId, args);
|
||||
}
|
||||
|
||||
// Compatible wrapper: CloseTask
|
||||
async CloseTask(taskId: string): Promise<boolean> {
|
||||
return this.closeTask(taskId);
|
||||
}
|
||||
|
||||
// Compatible wrapper: OpenTask
|
||||
async OpenTask(taskId: string): Promise<boolean> {
|
||||
return this.reopenTask(taskId);
|
||||
}
|
||||
|
||||
// Compatible wrapper: GetAllProjects
|
||||
async GetAllProjects(): Promise<any[]> {
|
||||
try {
|
||||
const data = await this.getAllResources();
|
||||
return data.projects || [];
|
||||
} catch (error) {
|
||||
console.error('Error getting all projects:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Compatible wrapper: GetTaskById
|
||||
async GetTaskById(taskId: string): Promise<any> {
|
||||
try {
|
||||
const data = await this.getAllResources();
|
||||
const tasks = data.items || [];
|
||||
return tasks.find((t: any) => t.id === taskId);
|
||||
} catch (error) {
|
||||
console.error('Error getting task by id:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Compatible wrapper: GetActiveTasks
|
||||
async GetActiveTasks(options?: {
|
||||
projectId?: string;
|
||||
section_id?: string;
|
||||
label?: string;
|
||||
filter?: string;
|
||||
lang?: string;
|
||||
ids?: string[];
|
||||
}): Promise<any[]> {
|
||||
try {
|
||||
const data = await this.getAllResources();
|
||||
let tasks = data.items || [];
|
||||
|
||||
if (options) {
|
||||
if (options.projectId) {
|
||||
tasks = tasks.filter((t: any) => t.project_id === options.projectId);
|
||||
}
|
||||
if (options.section_id) {
|
||||
tasks = tasks.filter((t: any) => t.section_id === options.section_id);
|
||||
}
|
||||
if (options.label) {
|
||||
tasks = tasks.filter((t: any) => t.labels && t.labels.includes(options.label));
|
||||
}
|
||||
if (options.ids && options.ids.length > 0) {
|
||||
tasks = tasks.filter((t: any) => options.ids!.includes(t.id));
|
||||
}
|
||||
}
|
||||
|
||||
return tasks;
|
||||
} catch (error) {
|
||||
console.error('Error getting active tasks:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Compatible wrapper: InitializeAPI (returns self for compatibility)
|
||||
initializeAPI(): TodoistSyncAPI {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Delete task using Sync API
|
||||
async deleteTask(taskId: string): Promise<boolean> {
|
||||
const command = {
|
||||
type: 'item_delete',
|
||||
uuid: this.generateUUID(),
|
||||
args: {
|
||||
id: taskId
|
||||
}
|
||||
};
|
||||
|
||||
await this.executeCommands([command]);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue