UNPKG

@yoryoboy/clickup-sdk

Version:

A modular JavaScript SDK for interacting with the ClickUp API

293 lines (249 loc) 9.94 kB
import Task from "./Task.js"; import buildQuery from "../utils/queryBuilder.js"; /** * Utility function to split an array into chunks of specified size * @param {Array} array - The array to split * @param {number} chunkSize - Size of each chunk * @returns {Array} Array of chunks */ function chunkArray(array, chunkSize) { const chunks = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } /** * Utility function to delay execution for a specified time * @param {number} ms - Milliseconds to delay * @returns {Promise} Promise that resolves after the delay */ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } class TaskManager { constructor(client) { this.client = client; } async getTasks(params = {}) { const { list_id, page, ...query } = params; if (!list_id) throw new Error("Missing list_id"); // Case 1: fetch all pages if (page === "all") { let currentPage = 0; let allTasks = []; let lastPage = false; do { const search = buildQuery({ ...query, page: currentPage }); const url = `/list/${list_id}/task?${search}`; const res = await this.client.get(url); allTasks.push(...res.data.tasks); lastPage = res.data.last_page; currentPage++; } while (!lastPage); return allTasks.map((t) => new Task(t)); } // Case 2: regular one-page fetch const search = buildQuery({ ...query, page }); const url = `/list/${list_id}/task?${search}`; const res = await this.client.get(url); return res.data.tasks.map((t) => new Task(t)); } async getFilteredTasks(params = {}) { const { team_id, page, custom_fields, ...query } = params; if (!team_id) throw new Error("Missing team_id"); // Handle custom_fields array by stringifying it if it's an array if (custom_fields && Array.isArray(custom_fields)) { query.custom_fields = JSON.stringify(custom_fields); } // Case 1: fetch all pages if (page === "all") { let currentPage = 0; let allTasks = []; let hasMore = true; do { const search = buildQuery({ ...query, page: currentPage }); const url = `/team/${team_id}/task?${search}`; const res = await this.client.get(url); if (!res.data.tasks || res.data.tasks.length === 0) { hasMore = false; } else { allTasks.push(...res.data.tasks); currentPage++; } } while (hasMore); return allTasks.map((t) => new Task(t)); } // Case 2: regular one-page fetch const search = buildQuery({ ...query, page }); const url = `/team/${team_id}/task?${search}`; const res = await this.client.get(url); return (res.data.tasks || []).map((t) => new Task(t)); } async updateTask(task_id, data = {}) { if (!task_id) throw new Error("Missing task_id"); const url = `/task/${task_id}`; const res = await this.client.put(url, data); return new Task(res.data); } /** * Create a new task in a specific list * @param {string} list_id - The ID of the list to create the task in * @param {Object} taskData - Task data object * @param {string} taskData.name - Task name (required) * @param {string} [taskData.description] - Task description * @param {Array<number>} [taskData.assignees] - Array of assignee user IDs * @param {boolean} [taskData.archived] - Whether the task is archived * @param {Array<string>} [taskData.tags] - Array of tags * @param {string} [taskData.status] - Task status * @param {number|null} [taskData.priority] - Task priority * @param {number} [taskData.due_date] - Due date (Unix timestamp in ms) * @param {boolean} [taskData.due_date_time] - Whether the due date includes time * @param {number} [taskData.time_estimate] - Time estimate in milliseconds * @param {number} [taskData.start_date] - Start date (Unix timestamp in ms) * @param {boolean} [taskData.start_date_time] - Whether the start date includes time * @param {number} [taskData.points] - Sprint points * @param {boolean} [taskData.notify_all] - Whether to notify all assignees * @param {string|null} [taskData.parent] - Parent task ID for subtasks * @param {string} [taskData.markdown_content] - Markdown formatted description * @param {string|null} [taskData.links_to] - Task ID to create a linked dependency * @param {boolean} [taskData.check_required_custom_fields] - Whether to enforce required custom fields * @param {Array<Object>} [taskData.custom_fields] - Custom fields array * @param {number} [taskData.custom_item_id] - Custom task type ID * @returns {Promise<Task>} The created task * @throws {Error} If list_id is missing or taskData.name is missing */ async createTask(list_id, taskData) { if (!list_id) throw new Error("Missing list_id"); if (!taskData.name) throw new Error("Task name is required"); const url = `/list/${list_id}/task`; const res = await this.client.post(url, taskData); return new Task(res.data); } /** * Create multiple tasks with rate limiting * @param {string} list_id - The ID of the list to create tasks in * @param {Array<Object>|Object} tasks - Single task object or array of task objects * @param {Object} options - Options for batch processing * @param {number} [options.batchSize=100] - Number of tasks to process per batch (max 100) * @param {number} [options.delayBetweenBatches=60000] - Delay between batches in milliseconds (default: 60000ms = 1 minute) * @param {Function} [options.onProgress] - Callback function for progress updates * @param {boolean} [options.verbose=false] - Whether to log progress to console * @returns {Promise<Array<Task>>} Array of created tasks * @throws {Error} If list_id is missing */ async createTasks(list_id, tasks, options = {}) { if (!list_id) throw new Error("Missing list_id"); // Default options const batchSize = options.batchSize || 100; const delayBetweenBatches = options.delayBetweenBatches || 60000; // 1 minute default const onProgress = options.onProgress || (() => {}); const verbose = options.verbose || false; // Handle single task case if (!Array.isArray(tasks)) { if (verbose) console.log("Creating a single task..."); const result = [await this.createTask(list_id, tasks)]; if (verbose) console.log("Task created successfully."); return result; } // Split tasks into batches const batches = chunkArray(tasks, batchSize); const createdTasks = []; const totalTasks = tasks.length; // Log batch information if (verbose) { console.log( `Creating ${totalTasks} tasks in ${batches.length} batches (${batchSize} tasks per batch)` ); console.log( `Delay between batches: ${delayBetweenBatches}ms (${ delayBetweenBatches / 1000 } seconds)` ); } // Process each batch with rate limiting for (let i = 0; i < batches.length; i++) { const batch = batches[i]; const batchNumber = i + 1; const tasksProcessed = i * batchSize; // Log batch start if (verbose) { console.log( `\nProcessing batch ${batchNumber}/${batches.length} (${batch.length} tasks)...` ); } // Notify progress onProgress({ type: "batchStart", batchNumber, totalBatches: batches.length, batchSize: batch.length, tasksProcessed, totalTasks, }); // Create tasks in current batch (in parallel) const startTime = Date.now(); const batchPromises = batch.map((taskData) => this.createTask(list_id, taskData) ); const batchResults = await Promise.all(batchPromises); const endTime = Date.now(); createdTasks.push(...batchResults); // Log batch completion if (verbose) { console.log( `Batch ${batchNumber}/${batches.length} completed in ${ (endTime - startTime) / 1000 } seconds` ); console.log( `Progress: ${ tasksProcessed + batch.length }/${totalTasks} tasks created (${Math.round( ((tasksProcessed + batch.length) / totalTasks) * 100 )}%)` ); } // Notify progress onProgress({ type: "batchComplete", batchNumber, totalBatches: batches.length, batchSize: batch.length, tasksProcessed: tasksProcessed + batch.length, totalTasks, batchDuration: endTime - startTime, }); // Delay before processing next batch (except for the last batch) if (i < batches.length - 1) { if (verbose) { console.log( `Waiting ${delayBetweenBatches / 1000} seconds before next batch...` ); } // Notify waiting onProgress({ type: "waiting", waitTime: delayBetweenBatches, nextBatch: batchNumber + 1, totalBatches: batches.length, }); await delay(delayBetweenBatches); } } // Log completion if (verbose) { console.log( `\nAll tasks created successfully! Created ${createdTasks.length} tasks in ${batches.length} batches.` ); } // Notify completion onProgress({ type: "complete", totalTasks: createdTasks.length, totalBatches: batches.length, }); return createdTasks; } } export default TaskManager;