@doist/todoist-sdk 15.3.1 → 15.4.0

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.
@@ -1,3 +1,4 @@
1
+ import { encodePathSegment } from '../utils/request-helpers.js';
1
2
  const BASE_URI = 'https://api.todoist.com';
2
3
  const TODOIST_URI = 'https://todoist.com';
3
4
  export const TODOIST_WEB_URI = 'https://app.todoist.com/app';
@@ -94,48 +95,48 @@ export const ENDPOINT_PRICES = 'prices';
94
95
  export const ENDPOINT_PRICING = 'pricing';
95
96
  // Workspace invitation actions (require invite_code parameter)
96
97
  export function getWorkspaceInvitationAcceptEndpoint(inviteCode) {
97
- return `workspaces/invitations/${inviteCode}/accept`;
98
+ return `workspaces/invitations/${encodePathSegment(inviteCode)}/accept`;
98
99
  }
99
100
  export function getWorkspaceInvitationRejectEndpoint(inviteCode) {
100
- return `workspaces/invitations/${inviteCode}/reject`;
101
+ return `workspaces/invitations/${encodePathSegment(inviteCode)}/reject`;
101
102
  }
102
103
  // Insights endpoints
103
104
  export function getProjectInsightsActivityStatsEndpoint(projectId) {
104
- return `projects/${projectId}/insights/activity_stats`;
105
+ return `projects/${encodePathSegment(projectId)}/insights/activity_stats`;
105
106
  }
106
107
  export function getProjectInsightsHealthEndpoint(projectId) {
107
- return `projects/${projectId}/insights/health`;
108
+ return `projects/${encodePathSegment(projectId)}/insights/health`;
108
109
  }
109
110
  export function getProjectInsightsHealthContextEndpoint(projectId) {
110
- return `projects/${projectId}/insights/health/context`;
111
+ return `projects/${encodePathSegment(projectId)}/insights/health/context`;
111
112
  }
112
113
  export function getProjectInsightsProgressEndpoint(projectId) {
113
- return `projects/${projectId}/insights/progress`;
114
+ return `projects/${encodePathSegment(projectId)}/insights/progress`;
114
115
  }
115
116
  export function getProjectInsightsHealthAnalyzeEndpoint(projectId) {
116
- return `projects/${projectId}/insights/health/analyze`;
117
+ return `projects/${encodePathSegment(projectId)}/insights/health/analyze`;
117
118
  }
118
119
  export function getWorkspaceInsightsEndpoint(workspaceId) {
119
- return `workspaces/${workspaceId}/insights`;
120
+ return `workspaces/${encodePathSegment(workspaceId)}/insights`;
120
121
  }
121
122
  // Workspace members
122
123
  export const ENDPOINT_WORKSPACE_MEMBERS = 'workspaces/members';
123
124
  // Workspace user management (require workspace_id and/or user_id parameters)
124
125
  export function getWorkspaceUserTasksEndpoint(workspaceId, userId) {
125
- return `workspaces/${workspaceId}/users/${userId}/tasks`;
126
+ return `workspaces/${encodePathSegment(workspaceId)}/users/${encodePathSegment(userId)}/tasks`;
126
127
  }
127
128
  export function getWorkspaceInviteUsersEndpoint(workspaceId) {
128
- return `workspaces/${workspaceId}/users/invite`;
129
+ return `workspaces/${encodePathSegment(workspaceId)}/users/invite`;
129
130
  }
130
131
  export function getWorkspaceUserEndpoint(workspaceId, userId) {
131
- return `workspaces/${workspaceId}/users/${userId}`;
132
+ return `workspaces/${encodePathSegment(workspaceId)}/users/${encodePathSegment(userId)}`;
132
133
  }
133
134
  // Workspace projects (require workspace_id parameter)
134
135
  export function getWorkspaceActiveProjectsEndpoint(workspaceId) {
135
- return `workspaces/${workspaceId}/projects/active`;
136
+ return `workspaces/${encodePathSegment(workspaceId)}/projects/active`;
136
137
  }
137
138
  export function getWorkspaceArchivedProjectsEndpoint(workspaceId) {
138
- return `workspaces/${workspaceId}/projects/archived`;
139
+ return `workspaces/${encodePathSegment(workspaceId)}/projects/archived`;
139
140
  }
140
141
  // App management endpoints (dev:app_console scope)
141
142
  export const ENDPOINT_REST_APPS = 'apps';
@@ -147,44 +148,45 @@ export const ENDPOINT_REST_USER_AUTHORIZATIONS_DELETE = 'user/authorizations/del
147
148
  /** Available app icon sizes (small 60x60, medium 300x300, large 600x600). */
148
149
  export const APP_ICON_SIZES = ['small', 'medium', 'large'];
149
150
  export function getAppEndpoint(appId) {
150
- return `apps/${appId}`;
151
+ return `apps/${encodePathSegment(appId)}`;
151
152
  }
152
153
  export function getAppSecretsEndpoint(appId) {
153
- return `apps/${appId}/secrets`;
154
+ return `apps/${encodePathSegment(appId)}/secrets`;
154
155
  }
155
156
  export function getAppClientSecretEndpoint(appId) {
156
- return `apps/${appId}/client_secret`;
157
+ return `apps/${encodePathSegment(appId)}/client_secret`;
157
158
  }
158
159
  export function getAppTokensEndpoint(appId) {
159
- return `apps/${appId}/tokens`;
160
+ return `apps/${encodePathSegment(appId)}/tokens`;
160
161
  }
161
162
  export function getAppTestTokenEndpoint(appId) {
162
- return `apps/${appId}/test_token`;
163
+ return `apps/${encodePathSegment(appId)}/test_token`;
163
164
  }
164
165
  export function getAppDistributionTokenEndpoint(appId) {
165
- return `apps/${appId}/distribution_token`;
166
+ return `apps/${encodePathSegment(appId)}/distribution_token`;
166
167
  }
167
168
  export function getAppVerificationTokenEndpoint(appId) {
168
- return `apps/${appId}/verification_token`;
169
+ return `apps/${encodePathSegment(appId)}/verification_token`;
169
170
  }
170
171
  export function getAppWebhookEndpoint(appId) {
171
- return `apps/${appId}/webhook`;
172
+ return `apps/${encodePathSegment(appId)}/webhook`;
172
173
  }
173
174
  export function getAppIconEndpoint(appId, size) {
174
- return size === 'medium' ? `apps/${appId}/icon` : `apps/${appId}/icon/${size}`;
175
+ const base = `apps/${encodePathSegment(appId)}/icon`;
176
+ return size === 'medium' ? base : `${base}/${size}`;
175
177
  }
176
178
  export function getAppInstallationEndpoint(installationId) {
177
- return `apps/installations/${installationId}`;
179
+ return `apps/installations/${encodePathSegment(installationId)}`;
178
180
  }
179
181
  export function getAppByDistributionTokenEndpoint(distributionToken) {
180
- return `apps/by_distribution_token/${distributionToken}`;
182
+ return `apps/by_distribution_token/${encodePathSegment(distributionToken)}`;
181
183
  }
182
184
  export function getUiExtensionEndpoint(uiExtensionId) {
183
- return `apps/ui_extensions/${uiExtensionId}`;
185
+ return `apps/ui_extensions/${encodePathSegment(uiExtensionId)}`;
184
186
  }
185
187
  export function getUiExtensionIconEndpoint(uiExtensionId) {
186
- return `apps/ui_extensions/${uiExtensionId}/icon`;
188
+ return `apps/ui_extensions/${encodePathSegment(uiExtensionId)}/icon`;
187
189
  }
188
190
  export function getUiExtensionsByIntegrationEndpoint(integrationId) {
189
- return `apps/ui_extensions/integration/${integrationId}`;
191
+ return `apps/ui_extensions/integration/${encodePathSegment(integrationId)}`;
190
192
  }
@@ -43,6 +43,18 @@ function getTodoistRequestError(args) {
43
43
  }
44
44
  return requestError;
45
45
  }
46
+ /**
47
+ * Normalises anything thrown while making an API request into the error a
48
+ * caller should see: a `TodoistRequestError` (carrying the HTTP status and
49
+ * response body when there is one) reported against `originalStack`, or a
50
+ * generic `Error` when the thrown value isn't an `Error` at all.
51
+ */
52
+ export function toTodoistRequestError(error, originalStack) {
53
+ if (!(error instanceof Error)) {
54
+ return new Error('An unknown error occurred during the request');
55
+ }
56
+ return getTodoistRequestError({ error, originalStack });
57
+ }
46
58
  function getRequestConfiguration(args) {
47
59
  const { baseURL, apiToken, requestId, customHeaders } = args;
48
60
  const authHeader = apiToken ? { Authorization: getAuthHeader(apiToken) } : undefined;
@@ -121,9 +133,6 @@ export async function request(args) {
121
133
  return Object.assign(Object.assign({}, response), { data: convertedData });
122
134
  }
123
135
  catch (error) {
124
- if (!(error instanceof Error)) {
125
- throw new Error('An unknown error occurred during the request');
126
- }
127
- throw getTodoistRequestError({ error, originalStack });
136
+ throw toTodoistRequestError(error, originalStack);
128
137
  }
129
138
  }
@@ -1,4 +1,24 @@
1
1
  import { z } from 'zod';
2
+ /**
3
+ * Known activity object types, using modern names (`task`, `comment`) rather
4
+ * than legacy API names (`item`, `note`).
5
+ */
6
+ export const ACTIVITY_OBJECT_TYPES = ['task', 'comment', 'project'];
7
+ /**
8
+ * Known activity event types.
9
+ */
10
+ export const ACTIVITY_EVENT_TYPES = [
11
+ 'added',
12
+ 'updated',
13
+ 'deleted',
14
+ 'completed',
15
+ 'uncompleted',
16
+ 'archived',
17
+ 'unarchived',
18
+ 'shared',
19
+ 'left',
20
+ 'moved',
21
+ ];
2
22
  /**
3
23
  * Flexible object containing event-specific data.
4
24
  * Uses z.record to accept any properties for forward compatibility.
@@ -20,6 +20,7 @@ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _ar
20
20
  };
21
21
  import { v4 as uuidv4 } from 'uuid';
22
22
  import { fetchWithRetry } from '../transport/fetch-with-retry.js';
23
+ import { toTodoistRequestError } from '../transport/http-client.js';
23
24
  import { camelCaseKeys } from './case-conversion.js';
24
25
  /**
25
26
  * Helper function to determine content-type from filename extension.
@@ -170,6 +171,8 @@ function buildMultipartBody(args) {
170
171
  * @param additionalFields - Additional form fields to include (e.g., project_id, workspace_id)
171
172
  * @param requestId - Optional request ID for idempotency
172
173
  * @returns The response data from the server
174
+ * @throws TodoistRequestError if the upload fails, carrying the HTTP status and
175
+ * the server's error body like any other API request.
173
176
  *
174
177
  * @example
175
178
  * ```typescript
@@ -197,6 +200,8 @@ function buildMultipartBody(args) {
197
200
  */
198
201
  export async function uploadMultipartFile(args) {
199
202
  const { baseUrl, authToken, endpoint, file, fileName, additionalFields, requestId, customFetch, } = args;
203
+ // Capture original stack for better error reporting
204
+ const originalStack = new Error();
200
205
  // Build the full URL
201
206
  const url = `${baseUrl}${endpoint}`;
202
207
  let body;
@@ -251,7 +256,15 @@ export async function uploadMultipartFile(args) {
251
256
  headers['Content-Type'] = multipart.contentType;
252
257
  body = multipart.body;
253
258
  // Make the request using fetch
254
- const response = await fetchWithRetry(Object.assign(Object.assign({ url, options: Object.assign({ method: 'POST', body,
255
- headers, timeout: 30000 }, (multipart.isStreamed ? { duplex: 'half' } : {})) }, (multipart.isStreamed ? { retryConfig: { retries: 0 } } : {})), { customFetch }));
259
+ let response;
260
+ try {
261
+ response = await fetchWithRetry(Object.assign(Object.assign({ url, options: Object.assign({ method: 'POST', body,
262
+ headers, timeout: 30000 }, (multipart.isStreamed ? { duplex: 'half' } : {})) }, (multipart.isStreamed ? { retryConfig: { retries: 0 } } : {})), { customFetch }));
263
+ }
264
+ catch (error) {
265
+ // `fetchWithRetry` throws a plain `Error` for non-2xx responses; convert
266
+ // it so callers get the same error type as every other API request.
267
+ throw toTodoistRequestError(error, originalStack);
268
+ }
256
269
  return camelCaseKeys(response.data);
257
270
  }
@@ -1,10 +1,36 @@
1
+ import { TodoistArgumentError } from '../types/errors.js';
1
2
  /**
2
- * Joins path segments using `/` separator.
3
- * @param segments A list of **valid** path segments.
3
+ * Encodes a caller-supplied value (an ID, a name) for use as one URL path segment.
4
+ *
5
+ * Without this, a value such as `../projects/123` would be joined into the path
6
+ * as-is and the URL would resolve to a different endpoint. `encodeURIComponent`
7
+ * escapes `/`, `?`, `#` and `%`, but leaves `.` alone, so the dot segments `.`
8
+ * and `..` (and the empty segment) are rejected outright. `,` is kept literal,
9
+ * as it is a valid path character and joins lists of IDs.
10
+ *
11
+ * @param segment The value to encode.
12
+ * @returns The encoded segment.
13
+ * @throws {TodoistArgumentError} When the value is empty, `.` or `..`.
14
+ */
15
+ export function encodePathSegment(segment) {
16
+ if (segment === '' || segment === '.' || segment === '..') {
17
+ throw new TodoistArgumentError(`Invalid path segment: "${segment}"`);
18
+ }
19
+ return encodeURIComponent(segment).replace(/%2C/gi, ',');
20
+ }
21
+ /**
22
+ * Joins an endpoint with path segments using `/` separator.
23
+ *
24
+ * The endpoint is used as-is (it may contain `/`); every following segment is
25
+ * encoded with {@link encodePathSegment}, so an ID can never address another
26
+ * resource.
27
+ *
28
+ * @param endpoint The base endpoint, e.g. `tasks`.
29
+ * @param segments Path segments to append, e.g. an ID and an action.
4
30
  * @returns A joined path.
5
31
  */
6
- export function generatePath(...segments) {
7
- return segments.join('/');
32
+ export function generatePath(endpoint, ...segments) {
33
+ return [endpoint, ...segments.map(encodePathSegment)].join('/');
8
34
  }
9
35
  /**
10
36
  * Returns `fn(value)` spread into an object if `value` is defined, otherwise an empty object.
@@ -11,6 +11,13 @@ type RequestArgs = {
11
11
  customFetch?: CustomFetch;
12
12
  };
13
13
  export declare function paramsSerializer(params: Record<string, unknown>): string;
14
+ /**
15
+ * Normalises anything thrown while making an API request into the error a
16
+ * caller should see: a `TodoistRequestError` (carrying the HTTP status and
17
+ * response body when there is one) reported against `originalStack`, or a
18
+ * generic `Error` when the thrown value isn't an `Error` at all.
19
+ */
20
+ export declare function toTodoistRequestError(error: unknown, originalStack?: Error): Error;
14
21
  export declare function isSuccess(response: HttpResponse): boolean;
15
22
  export declare function request<T>(args: RequestArgs): Promise<HttpResponse<T>>;
16
23
  export {};
@@ -1,13 +1,21 @@
1
1
  import { z } from 'zod';
2
+ /**
3
+ * Known activity object types, using modern names (`task`, `comment`) rather
4
+ * than legacy API names (`item`, `note`).
5
+ */
6
+ export declare const ACTIVITY_OBJECT_TYPES: readonly ["task", "comment", "project"];
7
+ /**
8
+ * Known activity event types.
9
+ */
10
+ export declare const ACTIVITY_EVENT_TYPES: readonly ["added", "updated", "deleted", "completed", "uncompleted", "archived", "unarchived", "shared", "left", "moved"];
2
11
  /**
3
12
  * Type hints for known object types. Accepts any string for forward compatibility.
4
13
  */
5
- export type ActivityObjectType = 'task' | 'comment' | 'project' | (string & Record<string, never>);
14
+ export type ActivityObjectType = (typeof ACTIVITY_OBJECT_TYPES)[number] | (string & Record<string, never>);
6
15
  /**
7
16
  * Type hints for known event types. Accepts any string for forward compatibility.
8
17
  */
9
- export type ActivityEventType = 'added' | 'updated' | 'deleted' | 'completed' | 'uncompleted' | 'archived' | 'unarchived' | 'shared' | 'left' | (string & Record<string, never>);
10
- type ModernActivityObjectType = 'task' | 'comment' | 'project' | (string & Record<string, never>);
18
+ export type ActivityEventType = (typeof ACTIVITY_EVENT_TYPES)[number] | (string & Record<string, never>);
11
19
  /**
12
20
  * Combined object:event filter string for the `objectEventTypes` parameter of `getActivityLogs`.
13
21
  *
@@ -21,7 +29,7 @@ type ModernActivityObjectType = 'task' | 'comment' | 'project' | (string & Recor
21
29
  * to be passed for forward compatibility, while still providing autocomplete for
22
30
  * the known combinations above.
23
31
  */
24
- export type ActivityObjectEventType = `${ModernActivityObjectType}:${ActivityEventType}` | `${ModernActivityObjectType}:` | `:${ActivityEventType}` | (string & Record<string, never>);
32
+ export type ActivityObjectEventType = `${ActivityObjectType}:${ActivityEventType}` | `${ActivityObjectType}:` | `:${ActivityEventType}` | (string & Record<string, never>);
25
33
  /**
26
34
  * Flexible object containing event-specific data.
27
35
  * Uses z.record to accept any properties for forward compatibility.
@@ -46,4 +54,3 @@ export declare const ActivityEventSchema: z.ZodObject<{
46
54
  * Represents an activity log event in Todoist.
47
55
  */
48
56
  export type ActivityEvent = z.infer<typeof ActivityEventSchema>;
49
- export {};
@@ -25,6 +25,8 @@ type UploadMultipartFileArgs = {
25
25
  * @param additionalFields - Additional form fields to include (e.g., project_id, workspace_id)
26
26
  * @param requestId - Optional request ID for idempotency
27
27
  * @returns The response data from the server
28
+ * @throws TodoistRequestError if the upload fails, carrying the HTTP status and
29
+ * the server's error body like any other API request.
28
30
  *
29
31
  * @example
30
32
  * ```typescript
@@ -1,9 +1,29 @@
1
1
  /**
2
- * Joins path segments using `/` separator.
3
- * @param segments A list of **valid** path segments.
2
+ * Encodes a caller-supplied value (an ID, a name) for use as one URL path segment.
3
+ *
4
+ * Without this, a value such as `../projects/123` would be joined into the path
5
+ * as-is and the URL would resolve to a different endpoint. `encodeURIComponent`
6
+ * escapes `/`, `?`, `#` and `%`, but leaves `.` alone, so the dot segments `.`
7
+ * and `..` (and the empty segment) are rejected outright. `,` is kept literal,
8
+ * as it is a valid path character and joins lists of IDs.
9
+ *
10
+ * @param segment The value to encode.
11
+ * @returns The encoded segment.
12
+ * @throws {TodoistArgumentError} When the value is empty, `.` or `..`.
13
+ */
14
+ export declare function encodePathSegment(segment: string): string;
15
+ /**
16
+ * Joins an endpoint with path segments using `/` separator.
17
+ *
18
+ * The endpoint is used as-is (it may contain `/`); every following segment is
19
+ * encoded with {@link encodePathSegment}, so an ID can never address another
20
+ * resource.
21
+ *
22
+ * @param endpoint The base endpoint, e.g. `tasks`.
23
+ * @param segments Path segments to append, e.g. an ID and an action.
4
24
  * @returns A joined path.
5
25
  */
6
- export declare function generatePath(...segments: string[]): string;
26
+ export declare function generatePath(endpoint: string, ...segments: string[]): string;
7
27
  /**
8
28
  * Returns `fn(value)` spread into an object if `value` is defined, otherwise an empty object.
9
29
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-sdk",
3
- "version": "15.3.1",
3
+ "version": "15.4.0",
4
4
  "description": "The official TypeScript SDK for the Todoist REST API.",
5
5
  "author": "Doist developers",
6
6
  "repository": "https://github.com/Doist/todoist-sdk-typescript",