@@ -30,6 +30,7 @@ import {
30
30
GetSharedLabelsResponse ,
31
31
GetCommentsResponse ,
32
32
QuickAddTaskResponse ,
33
+ type MoveTaskArgs ,
33
34
} from './types/requests'
34
35
import { request , isSuccess } from './restClient'
35
36
import { getTaskFromQuickAddResponse } from './utils/taskConverters'
@@ -47,6 +48,7 @@ import {
47
48
ENDPOINT_REST_LABELS_SHARED ,
48
49
ENDPOINT_REST_LABELS_SHARED_RENAME ,
49
50
ENDPOINT_REST_LABELS_SHARED_REMOVE ,
51
+ ENDPOINT_SYNC ,
50
52
} from './consts/endpoints'
51
53
import {
52
54
validateComment ,
@@ -63,6 +65,12 @@ import {
63
65
} from './utils/validators'
64
66
import { z } from 'zod'
65
67
68
+ import { v4 as uuidv4 } from 'uuid'
69
+ import { SyncResponse , type Command , type SyncRequest } from './types/sync'
70
+ import { TodoistRequestError } from './types'
71
+
72
+ const MAX_COMMAND_COUNT = 100
73
+
66
74
/**
67
75
* Joins path segments using `/` separator.
68
76
* @param segments A list of **valid** path segments.
@@ -211,6 +219,67 @@ export class TodoistApi {
211
219
return validateTask ( response . data )
212
220
}
213
221
222
+ /**
223
+ * Moves existing tasks by their ID to either a different parent/section/project.
224
+ *
225
+ * @param ids - The unique identifier of the tasks to be moved.
226
+ * @param args - The paramets that should contain only one of projectId, sectionId, or parentId
227
+ * @param requestId - Optional unique identifier for idempotency.
228
+ * @returns - A promise that resolves to an array of the updated tasks.
229
+ */
230
+ async moveTasks ( ids : string [ ] , args : MoveTaskArgs , requestId ?: string ) : Promise < Task [ ] > {
231
+ if ( ids . length > MAX_COMMAND_COUNT ) {
232
+ throw new TodoistRequestError ( `Maximum number of items is ${ MAX_COMMAND_COUNT } ` , 400 )
233
+ }
234
+ const uuid = uuidv4 ( )
235
+ const commands : Command [ ] = ids . map ( ( id ) => ( {
236
+ type : 'item_move' ,
237
+ uuid,
238
+ args : {
239
+ id,
240
+ ...( args . projectId && { project_id : args . projectId } ) ,
241
+ ...( args . sectionId && { section_id : args . sectionId } ) ,
242
+ ...( args . parentId && { parent_id : args . parentId } ) ,
243
+ } ,
244
+ } ) )
245
+
246
+ const syncRequest : SyncRequest = {
247
+ commands,
248
+ resource_types : [ 'items' ] ,
249
+ }
250
+
251
+ const response = await request < SyncResponse > (
252
+ 'POST' ,
253
+ this . syncApiBase ,
254
+ ENDPOINT_SYNC ,
255
+ this . authToken ,
256
+ syncRequest ,
257
+ requestId ,
258
+ /*hasSyncCommands: */ true ,
259
+ )
260
+
261
+ if ( response . data . sync_status ) {
262
+ Object . entries ( response . data . sync_status ) . forEach ( ( [ _ , value ] ) => {
263
+ if ( value === 'ok' ) return
264
+
265
+ throw new TodoistRequestError ( value . error , value . http_code , value . error_extra )
266
+ } )
267
+ }
268
+
269
+ if ( ! response . data . items ?. length ) {
270
+ throw new TodoistRequestError ( 'Tasks not found' , 404 )
271
+ }
272
+
273
+ const syncTasks = response . data . items . filter ( ( task ) => ids . includes ( task . id ) )
274
+ if ( ! syncTasks . length ) {
275
+ throw new TodoistRequestError ( 'Tasks not found' , 404 )
276
+ }
277
+
278
+ const tasks = syncTasks . map ( getTaskFromQuickAddResponse )
279
+
280
+ return validateTaskArray ( tasks )
281
+ }
282
+
214
283
/**
215
284
* Closes (completes) a task by its ID.
216
285
*
0 commit comments