Skip to main content
Glama
batch.ts12.1 kB
/** * Batch operations handler with temp ID management * Handles Todoist sync API batch operations with dependency resolution */ import { TodoistApiService, SyncResponse } from './todoist-api.js'; import { BatchOperationResult, BatchOperationError, TodoistAPIError, TodoistErrorCode, } from '../types/errors.js'; import { randomUUID } from 'crypto'; /** * Batch command types supported by Todoist sync API */ export type BatchCommandType = | 'item_add' | 'item_update' | 'item_delete' | 'item_complete' | 'item_uncomplete' | 'item_move' | 'project_add' | 'project_update' | 'project_delete' | 'project_archive' | 'section_add' | 'section_update' | 'section_delete'; /** * Batch command structure for Todoist sync API */ export interface BatchCommand { type: BatchCommandType; temp_id?: string; uuid: string; args: BatchCommandArgs; [key: string]: unknown; } type BatchCommandArgs = Record<string, unknown>; type SectionIndexedTaskInput = BatchCommandArgs & { section_index?: number }; /** * Dependency relationship between commands */ interface CommandDependency { commandIndex: number; dependsOn: string; // temp_id that this command depends on field: string; // field name that will be resolved } /** * Batch operation request with dependency tracking */ export interface BatchOperationRequest { commands: BatchCommand[]; dependencies?: CommandDependency[]; options?: { continueOnError?: boolean; validateOnly?: boolean; timeout?: number; }; } /** * Batch operations service for managing complex multi-command operations */ export class BatchOperationsService { private readonly apiService: TodoistApiService; private readonly maxBatchSize = 100; // Todoist API limit constructor(apiService: TodoistApiService) { this.apiService = apiService; } /** * Execute a batch of commands with dependency resolution */ async executeBatch( request: BatchOperationRequest ): Promise<BatchOperationResult> { const { commands, dependencies = [], options = {} } = request; // Validate batch size if (commands.length === 0) { throw new TodoistAPIError( TodoistErrorCode.VALIDATION_ERROR, 'Batch cannot be empty', {}, false ); } if (commands.length > this.maxBatchSize) { throw new TodoistAPIError( TodoistErrorCode.VALIDATION_ERROR, `Batch size exceeds maximum of ${this.maxBatchSize} commands`, { provided: commands.length, maximum: this.maxBatchSize }, false ); } // Generate UUIDs for commands that don't have them const processedCommands = this.ensureCommandUUIDs(commands); // Validate dependencies this.validateDependencies(processedCommands, dependencies); // Sort commands by dependencies const sortedCommands = this.resolveDependencyOrder( processedCommands, dependencies ); if (options.validateOnly) { return { success: true, completed_commands: sortedCommands.length, failed_commands: 0, errors: [], temp_id_mapping: {}, }; } try { // Execute the batch via Todoist sync API const syncResponse = await this.apiService.sync(sortedCommands); return this.processSyncResponse( syncResponse, sortedCommands, options.continueOnError ); } catch (error) { return this.handleBatchError(error, sortedCommands); } } /** * Create a batch operation builder for fluent API */ createBatch(): BatchBuilder { return new BatchBuilder(this); } /** * Helper methods for common batch operations */ /** * Create a project with tasks and sections in one batch */ async createProjectWithStructure( projectData: BatchCommandArgs, sections: BatchCommandArgs[] = [], tasks: SectionIndexedTaskInput[] = [] ): Promise<BatchOperationResult> { const batch = this.createBatch(); // Add project const projectTempId = batch.addProject(projectData); // Add sections const sectionTempIds: string[] = []; for (const sectionData of sections) { const sectionTempId = batch.addSection({ ...sectionData, project_id: projectTempId, }); sectionTempIds.push(sectionTempId); } // Add tasks for (let i = 0; i < tasks.length; i++) { const taskData = tasks[i]; const sectionIndex = typeof taskData.section_index === 'number' ? taskData.section_index : undefined; const sectionId = sectionIndex !== undefined ? sectionTempIds[sectionIndex] : undefined; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { section_index, ...taskPayload } = taskData; batch.addTask({ ...taskPayload, project_id: projectTempId, section_id: sectionId, }); } return batch.execute(); } /** * Move tasks between projects/sections in batch */ async moveTasks( taskIds: string[], targetProjectId: string, targetSectionId?: string ): Promise<BatchOperationResult> { const batch = this.createBatch(); for (const taskId of taskIds) { batch.updateTask(taskId, { project_id: targetProjectId, section_id: targetSectionId, }); } return batch.execute(); } /** * Complete multiple tasks at once */ async completeTasks(taskIds: string[]): Promise<BatchOperationResult> { const batch = this.createBatch(); for (const taskId of taskIds) { batch.completeTask(taskId); } return batch.execute(); } private ensureCommandUUIDs(commands: BatchCommand[]): BatchCommand[] { return commands.map(cmd => ({ ...cmd, uuid: cmd.uuid || randomUUID(), })); } private validateDependencies( commands: BatchCommand[], dependencies: CommandDependency[] ): void { const tempIds = new Set(commands.map(cmd => cmd.temp_id).filter(Boolean)); const commandIndices = new Set(commands.map((_, index) => index)); for (const dep of dependencies) { if (!commandIndices.has(dep.commandIndex)) { throw new TodoistAPIError( TodoistErrorCode.VALIDATION_ERROR, `Invalid dependency: command index ${dep.commandIndex} does not exist`, { dependency: dep }, false ); } if (!tempIds.has(dep.dependsOn)) { throw new TodoistAPIError( TodoistErrorCode.VALIDATION_ERROR, `Invalid dependency: temp_id '${dep.dependsOn}' does not exist`, { dependency: dep }, false ); } } } private resolveDependencyOrder( commands: BatchCommand[], _dependencies: CommandDependency[] ): BatchCommand[] { // Simple topological sort for dependency resolution // For now, return commands in original order // TODO: Implement proper topological sorting if complex dependencies are needed return commands; } private processSyncResponse( syncResponse: SyncResponse, commands: BatchCommand[], continueOnError: boolean = false ): BatchOperationResult { const result: BatchOperationResult = { success: true, completed_commands: 0, failed_commands: 0, errors: [], temp_id_mapping: {}, }; // Process sync token mapping if (syncResponse.temp_id_mapping) { result.temp_id_mapping = syncResponse.temp_id_mapping; } // Process command results if (syncResponse.sync_status) { for (let i = 0; i < commands.length; i++) { const command = commands[i]; const status = syncResponse.sync_status?.[command.uuid]; if (status === 'ok') { result.completed_commands++; } else { result.failed_commands++; result.errors.push({ command_index: i, temp_id: command.temp_id, error: new TodoistAPIError( TodoistErrorCode.SYNC_ERROR, `Command failed: ${status}`, { command, status }, false ).toTodoistError(), }); if (!continueOnError) { result.success = false; break; } } } } return result; } private handleBatchError( error: unknown, commands: BatchCommand[] ): BatchOperationResult { const batchError: BatchOperationError = { command_index: -1, // Indicates batch-level error error: error instanceof TodoistAPIError ? error.toTodoistError() : new TodoistAPIError( TodoistErrorCode.SYNC_ERROR, 'Batch operation failed', { originalError: error }, false ).toTodoistError(), }; return { success: false, completed_commands: 0, failed_commands: commands.length, errors: [batchError], temp_id_mapping: {}, }; } } /** * Fluent builder for batch operations */ export class BatchBuilder { private commands: BatchCommand[] = []; private dependencies: CommandDependency[] = []; private tempIdCounter = 0; constructor(private batchService: BatchOperationsService) {} /** * Add a task creation command */ addTask(taskData: BatchCommandArgs): string { const tempId = this.generateTempId('task'); this.commands.push({ type: 'item_add', temp_id: tempId, uuid: randomUUID(), args: taskData, }); return tempId; } /** * Update an existing task */ updateTask(taskId: string, updates: BatchCommandArgs): this { this.commands.push({ type: 'item_update', uuid: randomUUID(), args: { id: taskId, ...updates }, }); return this; } /** * Delete a task */ deleteTask(taskId: string): this { this.commands.push({ type: 'item_delete', uuid: randomUUID(), args: { id: taskId }, }); return this; } /** * Complete a task */ completeTask(taskId: string): this { this.commands.push({ type: 'item_complete', uuid: randomUUID(), args: { id: taskId }, }); return this; } /** * Add a project creation command */ addProject(projectData: BatchCommandArgs): string { const tempId = this.generateTempId('project'); this.commands.push({ type: 'project_add', temp_id: tempId, uuid: randomUUID(), args: projectData, }); return tempId; } /** * Update an existing project */ updateProject(projectId: string, updates: BatchCommandArgs): this { this.commands.push({ type: 'project_update', uuid: randomUUID(), args: { id: projectId, ...updates }, }); return this; } /** * Add a section creation command */ addSection(sectionData: BatchCommandArgs): string { const tempId = this.generateTempId('section'); this.commands.push({ type: 'section_add', temp_id: tempId, uuid: randomUUID(), args: sectionData, }); return tempId; } /** * Update an existing section */ updateSection(sectionId: string, updates: BatchCommandArgs): this { this.commands.push({ type: 'section_update', uuid: randomUUID(), args: { id: sectionId, ...updates }, }); return this; } /** * Execute the batch */ async execute(options?: { continueOnError?: boolean; }): Promise<BatchOperationResult> { return this.batchService.executeBatch({ commands: this.commands, dependencies: this.dependencies, options, }); } /** * Get the current batch size */ size(): number { return this.commands.length; } /** * Clear all commands */ clear(): this { this.commands = []; this.dependencies = []; this.tempIdCounter = 0; return this; } private generateTempId(type: string): string { return `temp_${type}_${++this.tempIdCounter}_${Date.now()}`; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shayonpal/mcp-todoist'

If you have feedback or need assistance with the MCP directory API, please join our Discord server