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:
HeroBlackInk 2026-02-16 13:10:10 +08:00
parent d7b4167468
commit 05b92a04f9
4 changed files with 294 additions and 20 deletions

View file

@ -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);

View file

@ -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;

View file

@ -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) {

View file

@ -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;
}
}