@taazkareem/clickup-mcp-server 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/build/index.js +1 -0
- package/build/logger.js +26 -1
- package/build/server.js +1 -1
- package/build/services/clickup/base.js +22 -1
- package/build/services/clickup/bulk.js +76 -45
- package/build/services/clickup/index.js +2 -2
- package/build/services/clickup/task/index.js +32 -0
- package/build/services/clickup/task/task-attachments.js +97 -0
- package/build/services/clickup/task/task-comments.js +104 -0
- package/build/services/clickup/task/task-core.js +477 -0
- package/build/services/clickup/task/task-custom-fields.js +97 -0
- package/build/services/clickup/task/task-search.js +462 -0
- package/build/services/clickup/task/task-service.js +25 -0
- package/build/services/clickup/task/task-tags.js +101 -0
- package/build/services/clickup/workspace.js +81 -36
- package/build/tools/folder.js +1 -1
- package/build/tools/list.js +2 -4
- package/build/tools/task/attachments.js +18 -5
- package/build/tools/task/attachments.types.js +9 -0
- package/build/tools/task/bulk-operations.js +111 -15
- package/build/tools/task/handlers.js +169 -24
- package/build/tools/task/index.js +1 -1
- package/build/tools/task/main.js +36 -1
- package/build/tools/task/single-operations.js +51 -4
- package/build/tools/task/utilities.js +24 -71
- package/build/tools/utils.js +2 -2
- package/build/utils/date-utils.js +149 -30
- package/build/utils/resolver-utils.js +33 -40
- package/build/utils/sponsor-service.js +1 -1
- package/package.json +1 -1
- package/build/services/clickup/task.js +0 -701
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* ClickUp Task Service - Core Module
|
|
6
|
+
*
|
|
7
|
+
* Handles core operations related to tasks in ClickUp, including:
|
|
8
|
+
* - Base service initialization
|
|
9
|
+
* - Core utility methods
|
|
10
|
+
* - Basic CRUD operations
|
|
11
|
+
*/
|
|
12
|
+
import { BaseClickUpService, ErrorCode, ClickUpServiceError } from '../base.js';
|
|
13
|
+
import { ListService } from '../list.js';
|
|
14
|
+
/**
|
|
15
|
+
* Core TaskService class providing basic task operations
|
|
16
|
+
*/
|
|
17
|
+
export class TaskServiceCore extends BaseClickUpService {
|
|
18
|
+
constructor(apiKey, teamId, baseUrl, workspaceService) {
|
|
19
|
+
super(apiKey, teamId, baseUrl);
|
|
20
|
+
this.workspaceService = null;
|
|
21
|
+
// Cache for validated tasks and lists
|
|
22
|
+
this.validationCache = {
|
|
23
|
+
tasks: new Map(),
|
|
24
|
+
lists: new Map()
|
|
25
|
+
};
|
|
26
|
+
// Cache TTL in milliseconds (5 minutes)
|
|
27
|
+
this.CACHE_TTL = 5 * 60 * 1000;
|
|
28
|
+
if (workspaceService) {
|
|
29
|
+
this.workspaceService = workspaceService;
|
|
30
|
+
this.logOperation('constructor', { usingSharedWorkspaceService: true });
|
|
31
|
+
}
|
|
32
|
+
// Initialize list service for list lookups
|
|
33
|
+
this.listService = new ListService(apiKey, teamId, baseUrl, this.workspaceService);
|
|
34
|
+
this.logOperation('constructor', { initialized: true });
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Helper method to handle errors consistently
|
|
38
|
+
* @param error The error that occurred
|
|
39
|
+
* @param message Optional custom error message
|
|
40
|
+
* @returns A ClickUpServiceError
|
|
41
|
+
*/
|
|
42
|
+
handleError(error, message) {
|
|
43
|
+
if (error instanceof ClickUpServiceError) {
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
return new ClickUpServiceError(message || `Task service error: ${error.message}`, ErrorCode.UNKNOWN, error);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build URL parameters from task filters
|
|
50
|
+
* @param filters Task filters to convert to URL parameters
|
|
51
|
+
* @returns URLSearchParams object
|
|
52
|
+
*/
|
|
53
|
+
buildTaskFilterParams(filters) {
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
// Add all filters to the query parameters
|
|
56
|
+
if (filters.include_closed)
|
|
57
|
+
params.append('include_closed', String(filters.include_closed));
|
|
58
|
+
if (filters.subtasks)
|
|
59
|
+
params.append('subtasks', String(filters.subtasks));
|
|
60
|
+
if (filters.page)
|
|
61
|
+
params.append('page', String(filters.page));
|
|
62
|
+
if (filters.order_by)
|
|
63
|
+
params.append('order_by', filters.order_by);
|
|
64
|
+
if (filters.reverse)
|
|
65
|
+
params.append('reverse', String(filters.reverse));
|
|
66
|
+
// Array parameters
|
|
67
|
+
if (filters.statuses && filters.statuses.length > 0) {
|
|
68
|
+
filters.statuses.forEach(status => params.append('statuses[]', status));
|
|
69
|
+
}
|
|
70
|
+
if (filters.assignees && filters.assignees.length > 0) {
|
|
71
|
+
filters.assignees.forEach(assignee => params.append('assignees[]', assignee));
|
|
72
|
+
}
|
|
73
|
+
// Team tasks endpoint specific parameters
|
|
74
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
75
|
+
filters.tags.forEach(tag => params.append('tags[]', tag));
|
|
76
|
+
}
|
|
77
|
+
if (filters.list_ids && filters.list_ids.length > 0) {
|
|
78
|
+
filters.list_ids.forEach(id => params.append('list_ids[]', id));
|
|
79
|
+
}
|
|
80
|
+
if (filters.folder_ids && filters.folder_ids.length > 0) {
|
|
81
|
+
filters.folder_ids.forEach(id => params.append('folder_ids[]', id));
|
|
82
|
+
}
|
|
83
|
+
if (filters.space_ids && filters.space_ids.length > 0) {
|
|
84
|
+
filters.space_ids.forEach(id => params.append('space_ids[]', id));
|
|
85
|
+
}
|
|
86
|
+
if (filters.archived !== undefined)
|
|
87
|
+
params.append('archived', String(filters.archived));
|
|
88
|
+
if (filters.include_closed_lists !== undefined)
|
|
89
|
+
params.append('include_closed_lists', String(filters.include_closed_lists));
|
|
90
|
+
if (filters.include_archived_lists !== undefined)
|
|
91
|
+
params.append('include_archived_lists', String(filters.include_archived_lists));
|
|
92
|
+
if (filters.include_compact_time_entries !== undefined)
|
|
93
|
+
params.append('include_compact_time_entries', String(filters.include_compact_time_entries));
|
|
94
|
+
// Date filters
|
|
95
|
+
if (filters.due_date_gt)
|
|
96
|
+
params.append('due_date_gt', String(filters.due_date_gt));
|
|
97
|
+
if (filters.due_date_lt)
|
|
98
|
+
params.append('due_date_lt', String(filters.due_date_lt));
|
|
99
|
+
if (filters.date_created_gt)
|
|
100
|
+
params.append('date_created_gt', String(filters.date_created_gt));
|
|
101
|
+
if (filters.date_created_lt)
|
|
102
|
+
params.append('date_created_lt', String(filters.date_created_lt));
|
|
103
|
+
if (filters.date_updated_gt)
|
|
104
|
+
params.append('date_updated_gt', String(filters.date_updated_gt));
|
|
105
|
+
if (filters.date_updated_lt)
|
|
106
|
+
params.append('date_updated_lt', String(filters.date_updated_lt));
|
|
107
|
+
// Handle custom fields if present
|
|
108
|
+
if (filters.custom_fields) {
|
|
109
|
+
Object.entries(filters.custom_fields).forEach(([key, value]) => {
|
|
110
|
+
params.append(`custom_fields[${key}]`, String(value));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return params;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract priority value from a task
|
|
117
|
+
* @param task The task to extract priority from
|
|
118
|
+
* @returns TaskPriority or null
|
|
119
|
+
*/
|
|
120
|
+
extractPriorityValue(task) {
|
|
121
|
+
if (!task.priority || !task.priority.id) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const priorityValue = parseInt(task.priority.id);
|
|
125
|
+
// Ensure it's in the valid range 1-4
|
|
126
|
+
if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return priorityValue;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Extract task data for creation/duplication
|
|
133
|
+
* @param task The source task
|
|
134
|
+
* @param nameOverride Optional override for the task name
|
|
135
|
+
* @returns CreateTaskData object
|
|
136
|
+
*/
|
|
137
|
+
extractTaskData(task, nameOverride) {
|
|
138
|
+
return {
|
|
139
|
+
name: nameOverride || task.name,
|
|
140
|
+
description: task.description || '',
|
|
141
|
+
status: task.status?.status,
|
|
142
|
+
priority: this.extractPriorityValue(task),
|
|
143
|
+
due_date: task.due_date ? Number(task.due_date) : undefined,
|
|
144
|
+
assignees: task.assignees?.map(a => a.id) || []
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Create a new task in the specified list
|
|
149
|
+
* @param listId The ID of the list to create the task in
|
|
150
|
+
* @param taskData The data for the new task
|
|
151
|
+
* @returns The created task
|
|
152
|
+
*/
|
|
153
|
+
async createTask(listId, taskData) {
|
|
154
|
+
this.logOperation('createTask', { listId, ...taskData });
|
|
155
|
+
try {
|
|
156
|
+
return await this.makeRequest(async () => {
|
|
157
|
+
const response = await this.client.post(`/list/${listId}/task`, taskData);
|
|
158
|
+
return response.data;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw this.handleError(error, `Failed to create task in list ${listId}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get a specific task by ID
|
|
167
|
+
* @param taskId The ID of the task to retrieve
|
|
168
|
+
* @returns The task details
|
|
169
|
+
*/
|
|
170
|
+
async getTask(taskId) {
|
|
171
|
+
this.logOperation('getTask', { taskId });
|
|
172
|
+
try {
|
|
173
|
+
return await this.makeRequest(async () => {
|
|
174
|
+
const response = await this.client.get(`/task/${taskId}`);
|
|
175
|
+
return response.data;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
throw this.handleError(error, `Failed to get task ${taskId}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get all tasks in a list with optional filtering
|
|
184
|
+
* @param listId The ID of the list to get tasks from
|
|
185
|
+
* @param filters Optional filters to apply
|
|
186
|
+
* @returns List of tasks matching the filters
|
|
187
|
+
*/
|
|
188
|
+
async getTasks(listId, filters = {}) {
|
|
189
|
+
this.logOperation('getTasks', { listId, filters });
|
|
190
|
+
try {
|
|
191
|
+
return await this.makeRequest(async () => {
|
|
192
|
+
const params = this.buildTaskFilterParams(filters);
|
|
193
|
+
const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
|
|
194
|
+
return response.data.tasks;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw this.handleError(error, `Failed to get tasks from list ${listId}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get subtasks of a specific task
|
|
203
|
+
* @param taskId The ID of the parent task
|
|
204
|
+
* @returns Array of subtask details
|
|
205
|
+
*/
|
|
206
|
+
async getSubtasks(taskId) {
|
|
207
|
+
this.logOperation('getSubtasks', { taskId });
|
|
208
|
+
try {
|
|
209
|
+
return await this.makeRequest(async () => {
|
|
210
|
+
const response = await this.client.get(`/task/${taskId}`);
|
|
211
|
+
// Return subtasks if present, otherwise empty array
|
|
212
|
+
return response.data.subtasks || [];
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw this.handleError(error, `Failed to get subtasks for task ${taskId}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get a task by its custom ID
|
|
221
|
+
* @param customTaskId The custom ID of the task (e.g., "ABC-123")
|
|
222
|
+
* @param listId Optional list ID to limit the search
|
|
223
|
+
* @returns The task details
|
|
224
|
+
*/
|
|
225
|
+
async getTaskByCustomId(customTaskId, listId) {
|
|
226
|
+
this.logOperation('getTaskByCustomId', { customTaskId, listId });
|
|
227
|
+
try {
|
|
228
|
+
return await this.makeRequest(async () => {
|
|
229
|
+
// Construct the URL with optional list ID
|
|
230
|
+
const url = `/task/custom_task_ids?custom_task_id=${encodeURIComponent(customTaskId)}${listId ? `&list_id=${listId}` : ''}`;
|
|
231
|
+
const response = await this.client.get(url);
|
|
232
|
+
return response.data;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw this.handleError(error, `Failed to get task with custom ID ${customTaskId}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Update an existing task
|
|
241
|
+
* @param taskId The ID of the task to update
|
|
242
|
+
* @param updateData The data to update
|
|
243
|
+
* @returns The updated task
|
|
244
|
+
*/
|
|
245
|
+
async updateTask(taskId, updateData) {
|
|
246
|
+
this.logOperation('updateTask', { taskId, ...updateData });
|
|
247
|
+
try {
|
|
248
|
+
return await this.makeRequest(async () => {
|
|
249
|
+
const response = await this.client.put(`/task/${taskId}`, updateData);
|
|
250
|
+
return response.data;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
throw this.handleError(error, `Failed to update task ${taskId}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Delete a task
|
|
259
|
+
* @param taskId The ID of the task to delete
|
|
260
|
+
* @returns A ServiceResponse indicating success
|
|
261
|
+
*/
|
|
262
|
+
async deleteTask(taskId) {
|
|
263
|
+
this.logOperation('deleteTask', { taskId });
|
|
264
|
+
try {
|
|
265
|
+
await this.makeRequest(async () => {
|
|
266
|
+
await this.client.delete(`/task/${taskId}`);
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
success: true,
|
|
270
|
+
data: undefined,
|
|
271
|
+
error: undefined
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
throw this.handleError(error, `Failed to delete task ${taskId}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Move a task to a different list
|
|
280
|
+
* @param taskId The ID of the task to move
|
|
281
|
+
* @param destinationListId The ID of the list to move the task to
|
|
282
|
+
* @returns The updated task
|
|
283
|
+
*/
|
|
284
|
+
async moveTask(taskId, destinationListId) {
|
|
285
|
+
const startTime = Date.now();
|
|
286
|
+
this.logOperation('moveTask', { taskId, destinationListId, operation: 'start' });
|
|
287
|
+
try {
|
|
288
|
+
// First, get both the task and list info in parallel
|
|
289
|
+
const [task, listResult] = await Promise.all([
|
|
290
|
+
this.validateTaskExists(taskId),
|
|
291
|
+
this.validateListExists(destinationListId).then(() => this.listService.getList(destinationListId))
|
|
292
|
+
]);
|
|
293
|
+
const originalTask = task;
|
|
294
|
+
const destinationList = listResult;
|
|
295
|
+
// Log parallel request timing
|
|
296
|
+
const parallelRequestTime = Date.now() - startTime;
|
|
297
|
+
this.logOperation('moveTask', {
|
|
298
|
+
taskId,
|
|
299
|
+
destinationListId,
|
|
300
|
+
operation: 'parallel_requests_complete',
|
|
301
|
+
timing: { parallelRequestTime }
|
|
302
|
+
});
|
|
303
|
+
const currentStatus = originalTask.status?.status;
|
|
304
|
+
const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
|
|
305
|
+
// Enhanced status mapping logic
|
|
306
|
+
let newStatus = currentStatus;
|
|
307
|
+
if (currentStatus && availableStatuses.length > 0) {
|
|
308
|
+
// Only map status if current status isn't available in destination list
|
|
309
|
+
if (!availableStatuses.includes(currentStatus)) {
|
|
310
|
+
// Try to find a similar status
|
|
311
|
+
const similarStatus = availableStatuses.find(s => s.toLowerCase().includes(currentStatus.toLowerCase()) ||
|
|
312
|
+
currentStatus.toLowerCase().includes(s.toLowerCase()));
|
|
313
|
+
// If no similar status found, use the first available status
|
|
314
|
+
newStatus = similarStatus || availableStatuses[0];
|
|
315
|
+
this.logger.debug('Status mapping', {
|
|
316
|
+
original: currentStatus,
|
|
317
|
+
mapped: newStatus,
|
|
318
|
+
available: availableStatuses
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Make the move request
|
|
323
|
+
const movedTask = await this.makeRequest(async () => {
|
|
324
|
+
const response = await this.client.post(`/task/${taskId}`, {
|
|
325
|
+
list: destinationListId,
|
|
326
|
+
status: newStatus
|
|
327
|
+
});
|
|
328
|
+
return response.data;
|
|
329
|
+
});
|
|
330
|
+
// Cache the moved task
|
|
331
|
+
this.validationCache.tasks.set(taskId, {
|
|
332
|
+
validatedAt: Date.now(),
|
|
333
|
+
task: movedTask
|
|
334
|
+
});
|
|
335
|
+
const totalTime = Date.now() - startTime;
|
|
336
|
+
this.logOperation('moveTask', {
|
|
337
|
+
taskId,
|
|
338
|
+
destinationListId,
|
|
339
|
+
operation: 'complete',
|
|
340
|
+
timing: {
|
|
341
|
+
totalTime,
|
|
342
|
+
parallelRequestTime,
|
|
343
|
+
moveOperationTime: totalTime - parallelRequestTime
|
|
344
|
+
},
|
|
345
|
+
statusMapping: {
|
|
346
|
+
original: currentStatus,
|
|
347
|
+
new: newStatus,
|
|
348
|
+
wasMapped: currentStatus !== newStatus
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
return movedTask;
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
// Log failure
|
|
355
|
+
this.logOperation('moveTask', {
|
|
356
|
+
taskId,
|
|
357
|
+
destinationListId,
|
|
358
|
+
operation: 'failed',
|
|
359
|
+
error: error.message,
|
|
360
|
+
timing: { totalTime: Date.now() - startTime }
|
|
361
|
+
});
|
|
362
|
+
throw this.handleError(error, 'Failed to move task');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Duplicate a task, optionally to a different list
|
|
367
|
+
* @param taskId The ID of the task to duplicate
|
|
368
|
+
* @param listId Optional ID of list to create duplicate in (defaults to same list)
|
|
369
|
+
* @returns The duplicated task
|
|
370
|
+
*/
|
|
371
|
+
async duplicateTask(taskId, listId) {
|
|
372
|
+
this.logOperation('duplicateTask', { taskId, listId });
|
|
373
|
+
try {
|
|
374
|
+
// Get source task and validate destination list if provided
|
|
375
|
+
const [sourceTask, _] = await Promise.all([
|
|
376
|
+
this.validateTaskExists(taskId),
|
|
377
|
+
listId ? this.validateListExists(listId) : Promise.resolve()
|
|
378
|
+
]);
|
|
379
|
+
// Create duplicate in specified list or original list
|
|
380
|
+
const targetListId = listId || sourceTask.list.id;
|
|
381
|
+
const taskData = this.extractTaskData(sourceTask);
|
|
382
|
+
return await this.createTask(targetListId, taskData);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
throw this.handleError(error, `Failed to duplicate task ${taskId}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Validate a task exists and cache the result
|
|
390
|
+
* @param taskId The ID of the task to validate
|
|
391
|
+
* @returns The validated task
|
|
392
|
+
*/
|
|
393
|
+
async validateTaskExists(taskId) {
|
|
394
|
+
// Check cache first
|
|
395
|
+
const cached = this.validationCache.tasks.get(taskId);
|
|
396
|
+
if (cached && Date.now() - cached.validatedAt < this.CACHE_TTL) {
|
|
397
|
+
this.logger.debug('Using cached task validation', { taskId });
|
|
398
|
+
return cached.task;
|
|
399
|
+
}
|
|
400
|
+
// Not in cache or expired, fetch task
|
|
401
|
+
const task = await this.getTask(taskId);
|
|
402
|
+
// Cache the validation result
|
|
403
|
+
this.validationCache.tasks.set(taskId, {
|
|
404
|
+
validatedAt: Date.now(),
|
|
405
|
+
task
|
|
406
|
+
});
|
|
407
|
+
return task;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Validate that multiple tasks exist
|
|
411
|
+
* @param taskIds Array of task IDs to validate
|
|
412
|
+
* @returns Map of task IDs to task objects
|
|
413
|
+
*/
|
|
414
|
+
async validateTasksExist(taskIds) {
|
|
415
|
+
const results = new Map();
|
|
416
|
+
const toFetch = [];
|
|
417
|
+
// Check cache first
|
|
418
|
+
for (const taskId of taskIds) {
|
|
419
|
+
const cached = this.validationCache.tasks.get(taskId);
|
|
420
|
+
if (cached && Date.now() - cached.validatedAt < this.CACHE_TTL) {
|
|
421
|
+
results.set(taskId, cached.task);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
toFetch.push(taskId);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (toFetch.length > 0) {
|
|
428
|
+
// Fetch uncached tasks in parallel batches
|
|
429
|
+
const batchSize = 5;
|
|
430
|
+
for (let i = 0; i < toFetch.length; i += batchSize) {
|
|
431
|
+
const batch = toFetch.slice(i, i + batchSize);
|
|
432
|
+
const tasks = await Promise.all(batch.map(id => this.getTask(id)));
|
|
433
|
+
// Cache and store results
|
|
434
|
+
tasks.forEach((task, index) => {
|
|
435
|
+
const taskId = batch[index];
|
|
436
|
+
this.validationCache.tasks.set(taskId, {
|
|
437
|
+
validatedAt: Date.now(),
|
|
438
|
+
task
|
|
439
|
+
});
|
|
440
|
+
results.set(taskId, task);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return results;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Validate a list exists and cache the result
|
|
448
|
+
* @param listId The ID of the list to validate
|
|
449
|
+
*/
|
|
450
|
+
async validateListExists(listId) {
|
|
451
|
+
// Check cache first
|
|
452
|
+
const cached = this.validationCache.lists.get(listId);
|
|
453
|
+
if (cached && Date.now() - cached.validatedAt < this.CACHE_TTL) {
|
|
454
|
+
this.logger.debug('Using cached list validation', { listId });
|
|
455
|
+
if (!cached.valid) {
|
|
456
|
+
throw new ClickUpServiceError(`List ${listId} does not exist`, ErrorCode.NOT_FOUND);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
await this.listService.getList(listId);
|
|
462
|
+
// Cache the successful validation
|
|
463
|
+
this.validationCache.lists.set(listId, {
|
|
464
|
+
validatedAt: Date.now(),
|
|
465
|
+
valid: true
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
// Cache the failed validation
|
|
470
|
+
this.validationCache.lists.set(listId, {
|
|
471
|
+
validatedAt: Date.now(),
|
|
472
|
+
valid: false
|
|
473
|
+
});
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* ClickUp Task Service - Custom Fields Module
|
|
6
|
+
*
|
|
7
|
+
* Handles custom fields operations for ClickUp tasks, including:
|
|
8
|
+
* - Setting custom field values
|
|
9
|
+
* - Retrieving custom field values
|
|
10
|
+
*/
|
|
11
|
+
import { TaskServiceTags } from './task-tags.js';
|
|
12
|
+
/**
|
|
13
|
+
* Custom fields functionality for the TaskService
|
|
14
|
+
*/
|
|
15
|
+
export class TaskServiceCustomFields extends TaskServiceTags {
|
|
16
|
+
/**
|
|
17
|
+
* Set a single custom field value on a task
|
|
18
|
+
*
|
|
19
|
+
* @param taskId ID of the task
|
|
20
|
+
* @param fieldId ID of the custom field
|
|
21
|
+
* @param value Value to set for the custom field
|
|
22
|
+
* @returns Success response
|
|
23
|
+
*/
|
|
24
|
+
async setCustomFieldValue(taskId, fieldId, value) {
|
|
25
|
+
this.logOperation('setCustomFieldValue', { taskId, fieldId, value });
|
|
26
|
+
try {
|
|
27
|
+
const payload = {
|
|
28
|
+
value
|
|
29
|
+
};
|
|
30
|
+
await this.client.post(`/task/${taskId}/field/${fieldId}`, payload);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw this.handleError(error, `Failed to set custom field "${fieldId}" value`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Set multiple custom field values on a task
|
|
39
|
+
*
|
|
40
|
+
* @param taskId ID of the task
|
|
41
|
+
* @param customFields Array of custom field ID and value pairs
|
|
42
|
+
* @returns Success response
|
|
43
|
+
*/
|
|
44
|
+
async setCustomFieldValues(taskId, customFields) {
|
|
45
|
+
this.logOperation('setCustomFieldValues', { taskId, customFields });
|
|
46
|
+
try {
|
|
47
|
+
// Execute each update sequentially
|
|
48
|
+
for (const field of customFields) {
|
|
49
|
+
await this.setCustomFieldValue(taskId, field.id, field.value);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw this.handleError(error, 'Failed to set custom field values');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get all custom field values for a task
|
|
59
|
+
*
|
|
60
|
+
* @param taskId ID of the task
|
|
61
|
+
* @returns Record mapping field IDs to their values
|
|
62
|
+
*/
|
|
63
|
+
async getCustomFieldValues(taskId) {
|
|
64
|
+
this.logOperation('getCustomFieldValues', { taskId });
|
|
65
|
+
try {
|
|
66
|
+
// We need to fetch the full task to get its custom fields
|
|
67
|
+
const task = await this.getTask(taskId);
|
|
68
|
+
return task.custom_fields || {};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw this.handleError(error, 'Failed to get custom field values');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get a specific custom field value for a task
|
|
76
|
+
*
|
|
77
|
+
* @param taskId ID of the task
|
|
78
|
+
* @param fieldId ID of the custom field
|
|
79
|
+
* @returns The value of the custom field
|
|
80
|
+
* @throws ClickUpServiceError if the field doesn't exist
|
|
81
|
+
*/
|
|
82
|
+
async getCustomFieldValue(taskId, fieldId) {
|
|
83
|
+
this.logOperation('getCustomFieldValue', { taskId, fieldId });
|
|
84
|
+
try {
|
|
85
|
+
const customFields = await this.getCustomFieldValues(taskId);
|
|
86
|
+
if (fieldId in customFields) {
|
|
87
|
+
return customFields[fieldId];
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw this.handleError(new Error(`Custom field "${fieldId}" not found on task`), `Custom field "${fieldId}" not found on task`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw this.handleError(error, `Failed to get custom field "${fieldId}" value`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|