Skip to content

Commit 0d540d0

Browse files
feat: Add workspace folders CRUD endpoints
Add SDK support for the /api/v1/folders REST API endpoints, enabling consumers to list, get, create, update, and delete workspace folders. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ff587a0 commit 0d540d0

File tree

7 files changed

+280
-0
lines changed

7 files changed

+280
-0
lines changed

src/consts/endpoints.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export const ENDPOINT_GET_TOKEN = 'access_token'
5858
export const ENDPOINT_REVOKE_TOKEN = 'access_tokens/revoke'
5959
export const ENDPOINT_REVOKE = 'revoke'
6060

61+
export const ENDPOINT_REST_FOLDERS = 'folders'
62+
6163
// Workspace endpoints
6264
export const ENDPOINT_WORKSPACE_INVITATIONS = 'workspaces/invitations'
6365
export const ENDPOINT_WORKSPACE_INVITATIONS_ALL = 'workspaces/invitations/all'

src/test-utils/test-defaults.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Deadline,
99
RawComment,
1010
PersonalProject,
11+
Folder,
1112
} from '../types'
1213
import { getProjectUrl, getTaskUrl, getSectionUrl } from '../utils/url-helpers'
1314

@@ -290,3 +291,12 @@ export const COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT = {
290291
...RAW_COMMENT_WITH_OPTIONALS_AS_NULL_PROJECT,
291292
taskId: undefined,
292293
}
294+
295+
export const DEFAULT_FOLDER: Folder = {
296+
id: '789',
297+
name: 'This is a folder',
298+
workspaceId: '100',
299+
defaultOrder: 3,
300+
childOrder: 3,
301+
isDeleted: false,
302+
}

src/todoist-api.folders.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { TodoistApi } from '.'
2+
import { DEFAULT_AUTH_TOKEN, DEFAULT_FOLDER } from './test-utils/test-defaults'
3+
import { getSyncBaseUri, ENDPOINT_REST_FOLDERS } from './consts/endpoints'
4+
import { server, http, HttpResponse } from './test-utils/msw-setup'
5+
6+
function getTarget() {
7+
return new TodoistApi(DEFAULT_AUTH_TOKEN)
8+
}
9+
10+
describe('TodoistApi folder endpoints', () => {
11+
describe('getFolder', () => {
12+
test('returns result from rest client', async () => {
13+
server.use(
14+
http.get(`${getSyncBaseUri()}${ENDPOINT_REST_FOLDERS}/789`, () => {
15+
return HttpResponse.json(DEFAULT_FOLDER, { status: 200 })
16+
}),
17+
)
18+
const api = getTarget()
19+
20+
const folder = await api.getFolder('789')
21+
22+
expect(folder).toEqual(DEFAULT_FOLDER)
23+
})
24+
})
25+
26+
describe('getFolders', () => {
27+
test('returns result from rest client with workspace_id param', async () => {
28+
const folders = [DEFAULT_FOLDER]
29+
server.use(
30+
http.get(`${getSyncBaseUri()}${ENDPOINT_REST_FOLDERS}`, ({ request }) => {
31+
const url = new URL(request.url)
32+
expect(url.searchParams.get('workspace_id')).toBe('100')
33+
return HttpResponse.json(
34+
{ results: folders, nextCursor: '123' },
35+
{ status: 200 },
36+
)
37+
}),
38+
)
39+
const api = getTarget()
40+
41+
const { results, nextCursor } = await api.getFolders({ workspaceId: 100 })
42+
43+
expect(results).toEqual(folders)
44+
expect(nextCursor).toBe('123')
45+
})
46+
})
47+
48+
describe('addFolder', () => {
49+
const DEFAULT_ADD_FOLDER_ARGS = {
50+
name: 'This is a folder',
51+
workspaceId: 100,
52+
}
53+
54+
test('returns result from rest client', async () => {
55+
server.use(
56+
http.post(`${getSyncBaseUri()}${ENDPOINT_REST_FOLDERS}`, () => {
57+
return HttpResponse.json(DEFAULT_FOLDER, { status: 200 })
58+
}),
59+
)
60+
const api = getTarget()
61+
62+
const folder = await api.addFolder(DEFAULT_ADD_FOLDER_ARGS)
63+
64+
expect(folder).toEqual(DEFAULT_FOLDER)
65+
})
66+
})
67+
68+
describe('updateFolder', () => {
69+
const DEFAULT_UPDATE_FOLDER_ARGS = { name: 'a new name' }
70+
71+
test('returns success result from rest client', async () => {
72+
const returnedFolder = {
73+
...DEFAULT_FOLDER,
74+
...DEFAULT_UPDATE_FOLDER_ARGS,
75+
id: '789',
76+
}
77+
server.use(
78+
http.post(`${getSyncBaseUri()}${ENDPOINT_REST_FOLDERS}/789`, () => {
79+
return HttpResponse.json(returnedFolder, { status: 200 })
80+
}),
81+
)
82+
const api = getTarget()
83+
84+
const response = await api.updateFolder('789', DEFAULT_UPDATE_FOLDER_ARGS)
85+
86+
expect(response).toEqual(returnedFolder)
87+
})
88+
})
89+
90+
describe('deleteFolder', () => {
91+
test('returns success result from rest client', async () => {
92+
server.use(
93+
http.delete(`${getSyncBaseUri()}${ENDPOINT_REST_FOLDERS}/789`, () => {
94+
return HttpResponse.json(undefined, { status: 204 })
95+
}),
96+
)
97+
const api = getTarget()
98+
99+
const response = await api.deleteFolder('789')
100+
101+
expect(response).toEqual(true)
102+
})
103+
})
104+
})

src/todoist-api.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
WorkspacePlanDetails,
1414
JoinWorkspaceResult,
1515
Workspace,
16+
Folder,
1617
} from './types/entities'
1718
import {
1819
AddCommentArgs,
@@ -72,6 +73,10 @@ import {
7273
WorkspaceLogoResponse,
7374
MoveProjectToWorkspaceArgs,
7475
MoveProjectToPersonalArgs,
76+
GetFoldersArgs,
77+
GetFoldersResponse,
78+
AddFolderArgs,
79+
UpdateFolderArgs,
7580
} from './types/requests'
7681
import { CustomFetch } from './types/http'
7782
import { request, isSuccess } from './rest-client'
@@ -118,6 +123,7 @@ import {
118123
ENDPOINT_WORKSPACE_USERS,
119124
getWorkspaceActiveProjectsEndpoint,
120125
getWorkspaceArchivedProjectsEndpoint,
126+
ENDPOINT_REST_FOLDERS,
121127
} from './consts/endpoints'
122128
import {
123129
validateAttachment,
@@ -141,6 +147,8 @@ import {
141147
validateWorkspacePlanDetails,
142148
validateJoinWorkspaceResult,
143149
validateWorkspaceArray,
150+
validateFolder,
151+
validateFolderArray,
144152
} from './utils/validators'
145153
import { formatDateToYYYYMMDD } from './utils/url-helpers'
146154
import { uploadMultipartFile } from './utils/multipart-upload'
@@ -1506,6 +1514,115 @@ export class TodoistApi {
15061514
return isSuccess(response)
15071515
}
15081516

1517+
/* Folder methods */
1518+
1519+
/**
1520+
* Retrieves a paginated list of folders.
1521+
*
1522+
* @param args - Optional filter parameters such as workspace ID.
1523+
* @returns A promise that resolves to a paginated response of folders.
1524+
*/
1525+
async getFolders(args?: GetFoldersArgs): Promise<GetFoldersResponse> {
1526+
const {
1527+
data: { results, nextCursor },
1528+
} = await request<GetFoldersResponse>({
1529+
httpMethod: 'GET',
1530+
baseUri: this.syncApiBase,
1531+
relativePath: ENDPOINT_REST_FOLDERS,
1532+
apiToken: this.authToken,
1533+
customFetch: this.customFetch,
1534+
payload: args,
1535+
})
1536+
1537+
return {
1538+
results: validateFolderArray(results),
1539+
nextCursor,
1540+
}
1541+
}
1542+
1543+
/**
1544+
* Retrieves a single folder by its ID.
1545+
*
1546+
* @param id - The unique identifier of the folder.
1547+
* @returns A promise that resolves to the requested folder.
1548+
*/
1549+
async getFolder(id: string): Promise<Folder> {
1550+
z.string().parse(id)
1551+
const response = await request<Folder>({
1552+
httpMethod: 'GET',
1553+
baseUri: this.syncApiBase,
1554+
relativePath: generatePath(ENDPOINT_REST_FOLDERS, id),
1555+
apiToken: this.authToken,
1556+
customFetch: this.customFetch,
1557+
})
1558+
1559+
return validateFolder(response.data)
1560+
}
1561+
1562+
/**
1563+
* Creates a new folder.
1564+
*
1565+
* @param args - Folder creation parameters including name and workspace ID.
1566+
* @param requestId - Optional custom identifier for the request.
1567+
* @returns A promise that resolves to the created folder.
1568+
*/
1569+
async addFolder(args: AddFolderArgs, requestId?: string): Promise<Folder> {
1570+
const response = await request<Folder>({
1571+
httpMethod: 'POST',
1572+
baseUri: this.syncApiBase,
1573+
relativePath: ENDPOINT_REST_FOLDERS,
1574+
apiToken: this.authToken,
1575+
customFetch: this.customFetch,
1576+
payload: args,
1577+
requestId: requestId,
1578+
})
1579+
1580+
return validateFolder(response.data)
1581+
}
1582+
1583+
/**
1584+
* Updates an existing folder by its ID.
1585+
*
1586+
* @param id - The unique identifier of the folder to update.
1587+
* @param args - Update parameters such as name or default order.
1588+
* @param requestId - Optional custom identifier for the request.
1589+
* @returns A promise that resolves to the updated folder.
1590+
*/
1591+
async updateFolder(id: string, args: UpdateFolderArgs, requestId?: string): Promise<Folder> {
1592+
z.string().parse(id)
1593+
const response = await request<Folder>({
1594+
httpMethod: 'POST',
1595+
baseUri: this.syncApiBase,
1596+
relativePath: generatePath(ENDPOINT_REST_FOLDERS, id),
1597+
apiToken: this.authToken,
1598+
customFetch: this.customFetch,
1599+
payload: args,
1600+
requestId: requestId,
1601+
})
1602+
1603+
return validateFolder(response.data)
1604+
}
1605+
1606+
/**
1607+
* Deletes a folder by its ID.
1608+
*
1609+
* @param id - The unique identifier of the folder to delete.
1610+
* @param requestId - Optional custom identifier for the request.
1611+
* @returns A promise that resolves to `true` if successful.
1612+
*/
1613+
async deleteFolder(id: string, requestId?: string): Promise<boolean> {
1614+
z.string().parse(id)
1615+
const response = await request({
1616+
httpMethod: 'DELETE',
1617+
baseUri: this.syncApiBase,
1618+
relativePath: generatePath(ENDPOINT_REST_FOLDERS, id),
1619+
apiToken: this.authToken,
1620+
customFetch: this.customFetch,
1621+
requestId: requestId,
1622+
})
1623+
return isSuccess(response)
1624+
}
1625+
15091626
/* Workspace methods */
15101627

15111628
/**

src/types/entities.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,3 +644,14 @@ export const WorkspaceSchema = z.object({
644644
})
645645

646646
export type Workspace = z.infer<typeof WorkspaceSchema>
647+
648+
export const FolderSchema = z.object({
649+
id: z.string(),
650+
name: z.string(),
651+
workspaceId: z.string(),
652+
defaultOrder: z.number().int(),
653+
childOrder: z.number().int(),
654+
isDeleted: z.boolean(),
655+
})
656+
657+
export type Folder = z.infer<typeof FolderSchema>

src/types/requests.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
ActivityObjectType,
66
Comment,
77
Duration,
8+
Folder,
89
Label,
910
PersonalProject,
1011
ProjectViewStyle,
@@ -769,3 +770,28 @@ export type AllWorkspaceInvitationsResponse = import('./entities').WorkspaceInvi
769770
* Response type for workspace logo upload.
770771
*/
771772
export type WorkspaceLogoResponse = Record<string, unknown> | null
773+
774+
// Folder-related types
775+
776+
export type GetFoldersArgs = {
777+
workspaceId?: number | null
778+
cursor?: string | null
779+
limit?: number
780+
}
781+
782+
export type GetFoldersResponse = {
783+
results: Folder[]
784+
nextCursor: string | null
785+
}
786+
787+
export type AddFolderArgs = {
788+
name: string
789+
workspaceId: number
790+
defaultOrder?: number | null
791+
childOrder?: number | null
792+
}
793+
794+
export type UpdateFolderArgs = {
795+
name?: string | null
796+
defaultOrder?: number | null
797+
}

src/utils/validators.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import {
66
UserSchema,
77
CurrentUserSchema,
88
TaskSchema,
9+
FolderSchema,
910
type Attachment,
1011
type Task,
1112
type Section,
1213
type Label,
1314
type Comment,
1415
type User,
1516
type CurrentUser,
17+
type Folder,
1618
type ProductivityStats,
1719
PersonalProjectSchema,
1820
WorkspaceProjectSchema,
@@ -165,3 +167,11 @@ export function validateWorkspace(input: unknown): Workspace {
165167
export function validateWorkspaceArray(input: unknown[]): Workspace[] {
166168
return input.map(validateWorkspace)
167169
}
170+
171+
export function validateFolder(input: unknown): Folder {
172+
return FolderSchema.parse(input)
173+
}
174+
175+
export function validateFolderArray(input: unknown[]): Folder[] {
176+
return input.map(validateFolder)
177+
}

0 commit comments

Comments
 (0)