@doist/todoist-sdk 10.2.0-next.2 → 10.2.0-next.4

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.
@@ -16,7 +16,6 @@ const zod_1 = require("zod");
16
16
  const endpoints_1 = require("../consts/endpoints");
17
17
  const http_client_1 = require("../transport/http-client");
18
18
  const apps_1 = require("../types/apps");
19
- const case_conversion_1 = require("../utils/case-conversion");
20
19
  const multipart_upload_1 = require("../utils/multipart-upload");
21
20
  const validators_1 = require("../utils/validators");
22
21
  const base_client_1 = require("./base-client");
@@ -145,7 +144,7 @@ class AppClient extends base_client_1.BaseClient {
145
144
  requestId: requestId,
146
145
  customFetch: this.customFetch,
147
146
  });
148
- return (0, validators_1.validateAppWithUserCount)((0, case_conversion_1.camelCaseKeys)(data));
147
+ return (0, validators_1.validateAppWithUserCount)(data);
149
148
  }
150
149
  async getAppTestToken(appId, requestId) {
151
150
  validators_1.IdSchema.parse(appId);
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BackupClient = void 0;
4
4
  const endpoints_1 = require("../consts/endpoints");
5
5
  const http_client_1 = require("../transport/http-client");
6
- const headers_1 = require("../utils/headers");
6
+ const file_response_1 = require("../utils/file-response");
7
7
  const validators_1 = require("../utils/validators");
8
8
  const base_client_1 = require("./base-client");
9
9
  /**
@@ -35,31 +35,13 @@ class BackupClient extends base_client_1.BaseClient {
35
35
  if (!response.ok) {
36
36
  throw new Error(`Failed to download backup: ${response.status} ${response.statusText}`);
37
37
  }
38
- const text = await response.text();
39
- const buffer = new TextEncoder().encode(text).buffer;
40
- return {
41
- ok: response.ok,
42
- status: response.status,
43
- statusText: response.statusText,
44
- headers: response.headers,
45
- text: () => Promise.resolve(text),
46
- json: () => response.json(),
47
- arrayBuffer: () => Promise.resolve(buffer),
48
- };
38
+ return (0, file_response_1.wrapAsFileResponse)(response, 'downloadBackup');
49
39
  }
50
40
  const response = await fetch(url, fetchOptions);
51
41
  if (!response.ok) {
52
42
  throw new Error(`Failed to download backup: ${response.status} ${response.statusText}`);
53
43
  }
54
- return {
55
- ok: response.ok,
56
- status: response.status,
57
- statusText: response.statusText,
58
- headers: (0, headers_1.headersToRecord)(response.headers),
59
- text: () => response.text(),
60
- json: () => response.json(),
61
- arrayBuffer: () => response.arrayBuffer(),
62
- };
44
+ return (0, file_response_1.wrapAsFileResponse)(response, 'downloadBackup');
63
45
  }
64
46
  }
65
47
  exports.BackupClient = BackupClient;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GoalsClient = void 0;
4
- const zod_1 = require("zod");
5
4
  const endpoints_1 = require("../consts/endpoints");
6
5
  const http_client_1 = require("../transport/http-client");
7
6
  const request_helpers_1 = require("../utils/request-helpers");
@@ -43,7 +42,7 @@ class GoalsClient extends base_client_1.BaseClient {
43
42
  };
44
43
  }
45
44
  async getGoal(id) {
46
- zod_1.z.string().parse(id);
45
+ validators_1.IdSchema.parse(id);
47
46
  const response = await (0, http_client_1.request)({
48
47
  httpMethod: 'GET',
49
48
  baseUri: this.syncApiBase,
@@ -66,7 +65,7 @@ class GoalsClient extends base_client_1.BaseClient {
66
65
  return (0, validators_1.validateGoal)(response.data);
67
66
  }
68
67
  async updateGoal(id, args, requestId) {
69
- zod_1.z.string().parse(id);
68
+ validators_1.IdSchema.parse(id);
70
69
  const response = await (0, http_client_1.request)({
71
70
  httpMethod: 'POST',
72
71
  baseUri: this.syncApiBase,
@@ -79,7 +78,7 @@ class GoalsClient extends base_client_1.BaseClient {
79
78
  return (0, validators_1.validateGoal)(response.data);
80
79
  }
81
80
  async deleteGoal(id, requestId) {
82
- zod_1.z.string().parse(id);
81
+ validators_1.IdSchema.parse(id);
83
82
  const response = await (0, http_client_1.request)({
84
83
  httpMethod: 'DELETE',
85
84
  baseUri: this.syncApiBase,
@@ -91,7 +90,7 @@ class GoalsClient extends base_client_1.BaseClient {
91
90
  return (0, http_client_1.isSuccess)(response);
92
91
  }
93
92
  async completeGoal(id, requestId) {
94
- zod_1.z.string().parse(id);
93
+ validators_1.IdSchema.parse(id);
95
94
  const response = await (0, http_client_1.request)({
96
95
  httpMethod: 'POST',
97
96
  baseUri: this.syncApiBase,
@@ -103,7 +102,7 @@ class GoalsClient extends base_client_1.BaseClient {
103
102
  return (0, validators_1.validateGoal)(response.data);
104
103
  }
105
104
  async uncompleteGoal(id, requestId) {
106
- zod_1.z.string().parse(id);
105
+ validators_1.IdSchema.parse(id);
107
106
  const response = await (0, http_client_1.request)({
108
107
  httpMethod: 'POST',
109
108
  baseUri: this.syncApiBase,
@@ -115,8 +114,8 @@ class GoalsClient extends base_client_1.BaseClient {
115
114
  return (0, validators_1.validateGoal)(response.data);
116
115
  }
117
116
  async linkTaskToGoal({ goalId, taskId }, requestId) {
118
- zod_1.z.string().parse(goalId);
119
- zod_1.z.string().parse(taskId);
117
+ validators_1.IdSchema.parse(goalId);
118
+ validators_1.IdSchema.parse(taskId);
120
119
  const response = await (0, http_client_1.request)({
121
120
  httpMethod: 'POST',
122
121
  baseUri: this.syncApiBase,
@@ -129,8 +128,8 @@ class GoalsClient extends base_client_1.BaseClient {
129
128
  return (0, validators_1.validateGoal)(response.data);
130
129
  }
131
130
  async unlinkTaskFromGoal({ goalId, taskId }, requestId) {
132
- zod_1.z.string().parse(goalId);
133
- zod_1.z.string().parse(taskId);
131
+ validators_1.IdSchema.parse(goalId);
132
+ validators_1.IdSchema.parse(taskId);
134
133
  const response = await (0, http_client_1.request)({
135
134
  httpMethod: 'DELETE',
136
135
  baseUri: this.syncApiBase,
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TemplateClient = void 0;
4
4
  const endpoints_1 = require("../consts/endpoints");
5
5
  const http_client_1 = require("../transport/http-client");
6
- const case_conversion_1 = require("../utils/case-conversion");
7
6
  const multipart_upload_1 = require("../utils/multipart-upload");
8
7
  const validators_1 = require("../utils/validators");
9
8
  const base_client_1 = require("./base-client");
@@ -53,7 +52,7 @@ class TemplateClient extends base_client_1.BaseClient {
53
52
  customFetch: this.customFetch,
54
53
  requestId,
55
54
  });
56
- return this.validateTemplateResponse((0, case_conversion_1.camelCaseKeys)(data));
55
+ return this.validateTemplateResponse(data);
57
56
  }
58
57
  async importTemplateIntoProject(args, requestId) {
59
58
  const { file, fileName, projectId } = args;
@@ -67,7 +66,7 @@ class TemplateClient extends base_client_1.BaseClient {
67
66
  customFetch: this.customFetch,
68
67
  requestId,
69
68
  });
70
- return this.validateTemplateResponse((0, case_conversion_1.camelCaseKeys)(data));
69
+ return this.validateTemplateResponse(data);
71
70
  }
72
71
  async importTemplateFromId(args, requestId) {
73
72
  const { data } = await (0, http_client_1.request)({
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UiExtensionClient = void 0;
4
4
  const endpoints_1 = require("../consts/endpoints");
5
5
  const http_client_1 = require("../transport/http-client");
6
- const case_conversion_1 = require("../utils/case-conversion");
7
6
  const multipart_upload_1 = require("../utils/multipart-upload");
8
7
  const validators_1 = require("../utils/validators");
9
8
  const base_client_1 = require("./base-client");
@@ -102,7 +101,7 @@ class UiExtensionClient extends base_client_1.BaseClient {
102
101
  requestId: requestId,
103
102
  customFetch: this.customFetch,
104
103
  });
105
- return (0, validators_1.validateUiExtension)((0, case_conversion_1.camelCaseKeys)(data));
104
+ return (0, validators_1.validateUiExtension)(data);
106
105
  }
107
106
  }
108
107
  exports.UiExtensionClient = UiExtensionClient;
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UploadClient = void 0;
4
4
  const endpoints_1 = require("../consts/endpoints");
5
5
  const http_client_1 = require("../transport/http-client");
6
- const headers_1 = require("../utils/headers");
6
+ const file_response_1 = require("../utils/file-response");
7
7
  const multipart_upload_1 = require("../utils/multipart-upload");
8
8
  const validators_1 = require("../utils/validators");
9
9
  const base_client_1 = require("./base-client");
@@ -71,32 +71,13 @@ class UploadClient extends base_client_1.BaseClient {
71
71
  if (!response.ok) {
72
72
  throw new Error(`Failed to fetch attachment: ${response.status} ${response.statusText}`);
73
73
  }
74
- // Convert text to ArrayBuffer for custom fetch implementations that lack arrayBuffer()
75
- const text = await response.text();
76
- const buffer = new TextEncoder().encode(text).buffer;
77
- return {
78
- ok: response.ok,
79
- status: response.status,
80
- statusText: response.statusText,
81
- headers: response.headers,
82
- text: () => Promise.resolve(text),
83
- json: () => response.json(),
84
- arrayBuffer: () => Promise.resolve(buffer),
85
- };
74
+ return (0, file_response_1.wrapAsFileResponse)(response, 'viewAttachment');
86
75
  }
87
76
  const response = await fetch(fileUrl, fetchOptions);
88
77
  if (!response.ok) {
89
78
  throw new Error(`Failed to fetch attachment: ${response.status} ${response.statusText}`);
90
79
  }
91
- return {
92
- ok: response.ok,
93
- status: response.status,
94
- statusText: response.statusText,
95
- headers: (0, headers_1.headersToRecord)(response.headers),
96
- text: () => response.text(),
97
- json: () => response.json(),
98
- arrayBuffer: () => response.arrayBuffer(),
99
- };
80
+ return (0, file_response_1.wrapAsFileResponse)(response, 'viewAttachment');
100
81
  }
101
82
  }
102
83
  exports.UploadClient = UploadClient;
@@ -48,6 +48,7 @@ function createObsidianFetchAdapter(requestUrl) {
48
48
  // Wrap Obsidian's direct properties as methods returning promises
49
49
  text: () => Promise.resolve(response.text),
50
50
  json: () => Promise.resolve(response.json),
51
+ arrayBuffer: () => Promise.resolve(response.arrayBuffer),
51
52
  };
52
53
  };
53
54
  }
@@ -945,33 +945,101 @@ class TodoistApi {
945
945
  return this.folderClient.deleteFolder(id, requestId);
946
946
  }
947
947
  // ── Goals ──
948
+ /**
949
+ * Retrieves a paginated list of goals.
950
+ *
951
+ * @param args - Optional filters including owner type, workspace ID, cursor, and limit.
952
+ * @returns A promise that resolves to a paginated list of goals.
953
+ */
948
954
  async getGoals(args) {
949
955
  return this.goalsClient.getGoals(args);
950
956
  }
957
+ /**
958
+ * Searches for goals matching a query.
959
+ *
960
+ * @param args - Search parameters including query string and optional owner type.
961
+ * @returns A promise that resolves to a paginated list of matching goals.
962
+ */
951
963
  async searchGoals(args) {
952
964
  return this.goalsClient.searchGoals(args);
953
965
  }
966
+ /**
967
+ * Retrieves a single goal by its ID.
968
+ *
969
+ * @param id - The unique identifier of the goal.
970
+ * @returns A promise that resolves to the goal.
971
+ */
954
972
  async getGoal(id) {
955
973
  return this.goalsClient.getGoal(id);
956
974
  }
975
+ /**
976
+ * Creates a new goal.
977
+ *
978
+ * @param args - Goal attributes including name and optional workspace, description, deadline, and responsible user.
979
+ * @param requestId - Optional custom identifier for the request.
980
+ * @returns A promise that resolves to the created goal.
981
+ */
957
982
  async addGoal(args, requestId) {
958
983
  return this.goalsClient.addGoal(args, requestId);
959
984
  }
985
+ /**
986
+ * Updates an existing goal.
987
+ *
988
+ * @param id - The unique identifier of the goal to update.
989
+ * @param args - Attributes to update on the goal.
990
+ * @param requestId - Optional custom identifier for the request.
991
+ * @returns A promise that resolves to the updated goal.
992
+ */
960
993
  async updateGoal(id, args, requestId) {
961
994
  return this.goalsClient.updateGoal(id, args, requestId);
962
995
  }
996
+ /**
997
+ * Deletes a goal by its ID.
998
+ *
999
+ * @param id - The unique identifier of the goal to delete.
1000
+ * @param requestId - Optional custom identifier for the request.
1001
+ * @returns A promise that resolves to `true` if successful.
1002
+ */
963
1003
  async deleteGoal(id, requestId) {
964
1004
  return this.goalsClient.deleteGoal(id, requestId);
965
1005
  }
1006
+ /**
1007
+ * Marks a goal as complete.
1008
+ *
1009
+ * @param id - The unique identifier of the goal to complete.
1010
+ * @param requestId - Optional custom identifier for the request.
1011
+ * @returns A promise that resolves to the updated goal.
1012
+ */
966
1013
  async completeGoal(id, requestId) {
967
1014
  return this.goalsClient.completeGoal(id, requestId);
968
1015
  }
1016
+ /**
1017
+ * Marks a previously completed goal as incomplete.
1018
+ *
1019
+ * @param id - The unique identifier of the goal to uncomplete.
1020
+ * @param requestId - Optional custom identifier for the request.
1021
+ * @returns A promise that resolves to the updated goal.
1022
+ */
969
1023
  async uncompleteGoal(id, requestId) {
970
1024
  return this.goalsClient.uncompleteGoal(id, requestId);
971
1025
  }
1026
+ /**
1027
+ * Links a task to a goal so it contributes to goal progress.
1028
+ *
1029
+ * @param args - The goal ID and task ID to link.
1030
+ * @param requestId - Optional custom identifier for the request.
1031
+ * @returns A promise that resolves to the updated goal.
1032
+ */
972
1033
  async linkTaskToGoal(args, requestId) {
973
1034
  return this.goalsClient.linkTaskToGoal(args, requestId);
974
1035
  }
1036
+ /**
1037
+ * Unlinks a previously linked task from a goal.
1038
+ *
1039
+ * @param args - The goal ID and task ID to unlink.
1040
+ * @param requestId - Optional custom identifier for the request.
1041
+ * @returns A promise that resolves to `true` if successful.
1042
+ */
975
1043
  async unlinkTaskFromGoal(args, requestId) {
976
1044
  return this.goalsClient.unlinkTaskFromGoal(args, requestId);
977
1045
  }
@@ -92,6 +92,7 @@ function convertResponseToCustomFetch(response) {
92
92
  headers: headersToObject(response.headers),
93
93
  text: () => clonedResponse.text(),
94
94
  json: () => response.json(),
95
+ arrayBuffer: () => response.arrayBuffer(),
95
96
  };
96
97
  }
97
98
  /**
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wrapAsFileResponse = wrapAsFileResponse;
4
+ const headers_1 = require("./headers");
5
+ /**
6
+ * Wraps either a native `Response` or a `CustomFetchResponse` into the SDK's
7
+ * `FileResponse` shape (binary endpoints: `viewAttachment`, `downloadBackup`).
8
+ *
9
+ * `arrayBuffer()` is resolved lazily. A customFetch implementation that omits
10
+ * `arrayBuffer()` is still valid for callers that only read `text()`/`json()`;
11
+ * the descriptive error is thrown only if the caller actually requests binary
12
+ * bytes. This avoids the lossy `text()` → `TextEncoder` round-trip that
13
+ * corrupts non-UTF-8 bytes (every `0x80`-`0xFF` byte not part of a valid UTF-8
14
+ * sequence is replaced with `U+FFFD`).
15
+ */
16
+ function wrapAsFileResponse(response, context) {
17
+ const headers = response.headers instanceof Headers ? (0, headers_1.headersToRecord)(response.headers) : response.headers;
18
+ return {
19
+ ok: response.ok,
20
+ status: response.status,
21
+ statusText: response.statusText,
22
+ headers,
23
+ text: () => response.text(),
24
+ json: () => response.json(),
25
+ arrayBuffer: () => {
26
+ if (typeof response.arrayBuffer !== 'function') {
27
+ throw new Error(`customFetch response must implement arrayBuffer() for ${context} (binary endpoint); reading the body via text() corrupts non-UTF-8 bytes`);
28
+ }
29
+ return response.arrayBuffer();
30
+ },
31
+ };
32
+ }
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.uploadMultipartFile = uploadMultipartFile;
37
37
  const fetch_with_retry_1 = require("../transport/fetch-with-retry");
38
+ const case_conversion_1 = require("./case-conversion");
38
39
  /**
39
40
  * Helper function to determine content-type from filename extension.
40
41
  * @param fileName - The filename to analyze
@@ -174,5 +175,5 @@ async function uploadMultipartFile(args) {
174
175
  },
175
176
  customFetch,
176
177
  });
177
- return response.data;
178
+ return (0, case_conversion_1.camelCaseKeys)(response.data);
178
179
  }
@@ -13,7 +13,6 @@ import { z } from 'zod';
13
13
  import { ENDPOINT_REST_APPS, ENDPOINT_REST_APPS_INSTALLATIONS, ENDPOINT_REST_USER_AUTHORIZATIONS, ENDPOINT_REST_USER_AUTHORIZATIONS_DELETE, getAppByDistributionTokenEndpoint, getAppClientSecretEndpoint, getAppDistributionTokenEndpoint, getAppEndpoint, getAppIconEndpoint, getAppInstallationEndpoint, getAppSecretsEndpoint, getAppTestTokenEndpoint, getAppTokensEndpoint, getAppVerificationTokenEndpoint, getAppWebhookEndpoint, } from '../consts/endpoints.js';
14
14
  import { isSuccess, request } from '../transport/http-client.js';
15
15
  import { webhookEventToWireName } from '../types/apps/index.js';
16
- import { camelCaseKeys } from '../utils/case-conversion.js';
17
16
  import { uploadMultipartFile } from '../utils/multipart-upload.js';
18
17
  import { IdSchema, validateApp, validateAppArray, validateAppByDistributionToken, validateAppDistributionToken, validateAppInstallation, validateAppInstallationArray, validateAppSecrets, validateAppTestToken, validateAppVerificationToken, validateAppWebhook, validateAppWithUserCount, validateUserAuthorizationArray, } from '../utils/validators.js';
19
18
  import { BaseClient } from './base-client.js';
@@ -142,7 +141,7 @@ export class AppClient extends BaseClient {
142
141
  requestId: requestId,
143
142
  customFetch: this.customFetch,
144
143
  });
145
- return validateAppWithUserCount(camelCaseKeys(data));
144
+ return validateAppWithUserCount(data);
146
145
  }
147
146
  async getAppTestToken(appId, requestId) {
148
147
  IdSchema.parse(appId);
@@ -1,6 +1,6 @@
1
1
  import { ENDPOINT_REST_BACKUPS, ENDPOINT_REST_BACKUPS_DOWNLOAD } from '../consts/endpoints.js';
2
2
  import { request } from '../transport/http-client.js';
3
- import { headersToRecord } from '../utils/headers.js';
3
+ import { wrapAsFileResponse } from '../utils/file-response.js';
4
4
  import { validateBackupArray } from '../utils/validators.js';
5
5
  import { BaseClient } from './base-client.js';
6
6
  /**
@@ -32,30 +32,12 @@ export class BackupClient extends BaseClient {
32
32
  if (!response.ok) {
33
33
  throw new Error(`Failed to download backup: ${response.status} ${response.statusText}`);
34
34
  }
35
- const text = await response.text();
36
- const buffer = new TextEncoder().encode(text).buffer;
37
- return {
38
- ok: response.ok,
39
- status: response.status,
40
- statusText: response.statusText,
41
- headers: response.headers,
42
- text: () => Promise.resolve(text),
43
- json: () => response.json(),
44
- arrayBuffer: () => Promise.resolve(buffer),
45
- };
35
+ return wrapAsFileResponse(response, 'downloadBackup');
46
36
  }
47
37
  const response = await fetch(url, fetchOptions);
48
38
  if (!response.ok) {
49
39
  throw new Error(`Failed to download backup: ${response.status} ${response.statusText}`);
50
40
  }
51
- return {
52
- ok: response.ok,
53
- status: response.status,
54
- statusText: response.statusText,
55
- headers: headersToRecord(response.headers),
56
- text: () => response.text(),
57
- json: () => response.json(),
58
- arrayBuffer: () => response.arrayBuffer(),
59
- };
41
+ return wrapAsFileResponse(response, 'downloadBackup');
60
42
  }
61
43
  }
@@ -1,8 +1,7 @@
1
- import { z } from 'zod';
2
1
  import { ENDPOINT_REST_GOALS, ENDPOINT_REST_GOALS_SEARCH, GOAL_COMPLETE, GOAL_TASKS, GOAL_UNCOMPLETE, } from '../consts/endpoints.js';
3
2
  import { isSuccess, request } from '../transport/http-client.js';
4
3
  import { generatePath } from '../utils/request-helpers.js';
5
- import { validateGoal, validateGoalArray } from '../utils/validators.js';
4
+ import { IdSchema, validateGoal, validateGoalArray } from '../utils/validators.js';
6
5
  import { BaseClient } from './base-client.js';
7
6
  /**
8
7
  * Internal sub-client handling all goal-domain endpoints.
@@ -40,7 +39,7 @@ export class GoalsClient extends BaseClient {
40
39
  };
41
40
  }
42
41
  async getGoal(id) {
43
- z.string().parse(id);
42
+ IdSchema.parse(id);
44
43
  const response = await request({
45
44
  httpMethod: 'GET',
46
45
  baseUri: this.syncApiBase,
@@ -63,7 +62,7 @@ export class GoalsClient extends BaseClient {
63
62
  return validateGoal(response.data);
64
63
  }
65
64
  async updateGoal(id, args, requestId) {
66
- z.string().parse(id);
65
+ IdSchema.parse(id);
67
66
  const response = await request({
68
67
  httpMethod: 'POST',
69
68
  baseUri: this.syncApiBase,
@@ -76,7 +75,7 @@ export class GoalsClient extends BaseClient {
76
75
  return validateGoal(response.data);
77
76
  }
78
77
  async deleteGoal(id, requestId) {
79
- z.string().parse(id);
78
+ IdSchema.parse(id);
80
79
  const response = await request({
81
80
  httpMethod: 'DELETE',
82
81
  baseUri: this.syncApiBase,
@@ -88,7 +87,7 @@ export class GoalsClient extends BaseClient {
88
87
  return isSuccess(response);
89
88
  }
90
89
  async completeGoal(id, requestId) {
91
- z.string().parse(id);
90
+ IdSchema.parse(id);
92
91
  const response = await request({
93
92
  httpMethod: 'POST',
94
93
  baseUri: this.syncApiBase,
@@ -100,7 +99,7 @@ export class GoalsClient extends BaseClient {
100
99
  return validateGoal(response.data);
101
100
  }
102
101
  async uncompleteGoal(id, requestId) {
103
- z.string().parse(id);
102
+ IdSchema.parse(id);
104
103
  const response = await request({
105
104
  httpMethod: 'POST',
106
105
  baseUri: this.syncApiBase,
@@ -112,8 +111,8 @@ export class GoalsClient extends BaseClient {
112
111
  return validateGoal(response.data);
113
112
  }
114
113
  async linkTaskToGoal({ goalId, taskId }, requestId) {
115
- z.string().parse(goalId);
116
- z.string().parse(taskId);
114
+ IdSchema.parse(goalId);
115
+ IdSchema.parse(taskId);
117
116
  const response = await request({
118
117
  httpMethod: 'POST',
119
118
  baseUri: this.syncApiBase,
@@ -126,8 +125,8 @@ export class GoalsClient extends BaseClient {
126
125
  return validateGoal(response.data);
127
126
  }
128
127
  async unlinkTaskFromGoal({ goalId, taskId }, requestId) {
129
- z.string().parse(goalId);
130
- z.string().parse(taskId);
128
+ IdSchema.parse(goalId);
129
+ IdSchema.parse(taskId);
131
130
  const response = await request({
132
131
  httpMethod: 'DELETE',
133
132
  baseUri: this.syncApiBase,
@@ -1,6 +1,5 @@
1
1
  import { ENDPOINT_REST_TEMPLATES_CREATE_FROM_FILE, ENDPOINT_REST_TEMPLATES_FILE, ENDPOINT_REST_TEMPLATES_IMPORT_FROM_FILE, ENDPOINT_REST_TEMPLATES_IMPORT_FROM_ID, ENDPOINT_REST_TEMPLATES_URL, } from '../consts/endpoints.js';
2
2
  import { request } from '../transport/http-client.js';
3
- import { camelCaseKeys } from '../utils/case-conversion.js';
4
3
  import { uploadMultipartFile } from '../utils/multipart-upload.js';
5
4
  import { validateCommentArray, validateProjectArray, validateSectionArray, validateTaskArray, } from '../utils/validators.js';
6
5
  import { BaseClient } from './base-client.js';
@@ -50,7 +49,7 @@ export class TemplateClient extends BaseClient {
50
49
  customFetch: this.customFetch,
51
50
  requestId,
52
51
  });
53
- return this.validateTemplateResponse(camelCaseKeys(data));
52
+ return this.validateTemplateResponse(data);
54
53
  }
55
54
  async importTemplateIntoProject(args, requestId) {
56
55
  const { file, fileName, projectId } = args;
@@ -64,7 +63,7 @@ export class TemplateClient extends BaseClient {
64
63
  customFetch: this.customFetch,
65
64
  requestId,
66
65
  });
67
- return this.validateTemplateResponse(camelCaseKeys(data));
66
+ return this.validateTemplateResponse(data);
68
67
  }
69
68
  async importTemplateFromId(args, requestId) {
70
69
  const { data } = await request({
@@ -1,6 +1,5 @@
1
1
  import { ENDPOINT_REST_APPS_UI_EXTENSIONS, ENDPOINT_REST_APPS_UI_EXTENSIONS_INSTALLED, getUiExtensionEndpoint, getUiExtensionIconEndpoint, getUiExtensionsByIntegrationEndpoint, } from '../consts/endpoints.js';
2
2
  import { isSuccess, request } from '../transport/http-client.js';
3
- import { camelCaseKeys } from '../utils/case-conversion.js';
4
3
  import { uploadMultipartFile } from '../utils/multipart-upload.js';
5
4
  import { IdSchema, validateUiExtension, validateUiExtensionArray } from '../utils/validators.js';
6
5
  import { BaseClient } from './base-client.js';
@@ -99,6 +98,6 @@ export class UiExtensionClient extends BaseClient {
99
98
  requestId: requestId,
100
99
  customFetch: this.customFetch,
101
100
  });
102
- return validateUiExtension(camelCaseKeys(data));
101
+ return validateUiExtension(data);
103
102
  }
104
103
  }
@@ -1,6 +1,6 @@
1
1
  import { ENDPOINT_REST_UPLOADS } from '../consts/endpoints.js';
2
2
  import { isSuccess, request } from '../transport/http-client.js';
3
- import { headersToRecord } from '../utils/headers.js';
3
+ import { wrapAsFileResponse } from '../utils/file-response.js';
4
4
  import { uploadMultipartFile } from '../utils/multipart-upload.js';
5
5
  import { validateAttachment } from '../utils/validators.js';
6
6
  import { BaseClient } from './base-client.js';
@@ -68,31 +68,12 @@ export class UploadClient extends BaseClient {
68
68
  if (!response.ok) {
69
69
  throw new Error(`Failed to fetch attachment: ${response.status} ${response.statusText}`);
70
70
  }
71
- // Convert text to ArrayBuffer for custom fetch implementations that lack arrayBuffer()
72
- const text = await response.text();
73
- const buffer = new TextEncoder().encode(text).buffer;
74
- return {
75
- ok: response.ok,
76
- status: response.status,
77
- statusText: response.statusText,
78
- headers: response.headers,
79
- text: () => Promise.resolve(text),
80
- json: () => response.json(),
81
- arrayBuffer: () => Promise.resolve(buffer),
82
- };
71
+ return wrapAsFileResponse(response, 'viewAttachment');
83
72
  }
84
73
  const response = await fetch(fileUrl, fetchOptions);
85
74
  if (!response.ok) {
86
75
  throw new Error(`Failed to fetch attachment: ${response.status} ${response.statusText}`);
87
76
  }
88
- return {
89
- ok: response.ok,
90
- status: response.status,
91
- statusText: response.statusText,
92
- headers: headersToRecord(response.headers),
93
- text: () => response.text(),
94
- json: () => response.json(),
95
- arrayBuffer: () => response.arrayBuffer(),
96
- };
77
+ return wrapAsFileResponse(response, 'viewAttachment');
97
78
  }
98
79
  }
@@ -45,6 +45,7 @@ export function createObsidianFetchAdapter(requestUrl) {
45
45
  // Wrap Obsidian's direct properties as methods returning promises
46
46
  text: () => Promise.resolve(response.text),
47
47
  json: () => Promise.resolve(response.json),
48
+ arrayBuffer: () => Promise.resolve(response.arrayBuffer),
48
49
  };
49
50
  };
50
51
  }
@@ -942,33 +942,101 @@ export class TodoistApi {
942
942
  return this.folderClient.deleteFolder(id, requestId);
943
943
  }
944
944
  // ── Goals ──
945
+ /**
946
+ * Retrieves a paginated list of goals.
947
+ *
948
+ * @param args - Optional filters including owner type, workspace ID, cursor, and limit.
949
+ * @returns A promise that resolves to a paginated list of goals.
950
+ */
945
951
  async getGoals(args) {
946
952
  return this.goalsClient.getGoals(args);
947
953
  }
954
+ /**
955
+ * Searches for goals matching a query.
956
+ *
957
+ * @param args - Search parameters including query string and optional owner type.
958
+ * @returns A promise that resolves to a paginated list of matching goals.
959
+ */
948
960
  async searchGoals(args) {
949
961
  return this.goalsClient.searchGoals(args);
950
962
  }
963
+ /**
964
+ * Retrieves a single goal by its ID.
965
+ *
966
+ * @param id - The unique identifier of the goal.
967
+ * @returns A promise that resolves to the goal.
968
+ */
951
969
  async getGoal(id) {
952
970
  return this.goalsClient.getGoal(id);
953
971
  }
972
+ /**
973
+ * Creates a new goal.
974
+ *
975
+ * @param args - Goal attributes including name and optional workspace, description, deadline, and responsible user.
976
+ * @param requestId - Optional custom identifier for the request.
977
+ * @returns A promise that resolves to the created goal.
978
+ */
954
979
  async addGoal(args, requestId) {
955
980
  return this.goalsClient.addGoal(args, requestId);
956
981
  }
982
+ /**
983
+ * Updates an existing goal.
984
+ *
985
+ * @param id - The unique identifier of the goal to update.
986
+ * @param args - Attributes to update on the goal.
987
+ * @param requestId - Optional custom identifier for the request.
988
+ * @returns A promise that resolves to the updated goal.
989
+ */
957
990
  async updateGoal(id, args, requestId) {
958
991
  return this.goalsClient.updateGoal(id, args, requestId);
959
992
  }
993
+ /**
994
+ * Deletes a goal by its ID.
995
+ *
996
+ * @param id - The unique identifier of the goal to delete.
997
+ * @param requestId - Optional custom identifier for the request.
998
+ * @returns A promise that resolves to `true` if successful.
999
+ */
960
1000
  async deleteGoal(id, requestId) {
961
1001
  return this.goalsClient.deleteGoal(id, requestId);
962
1002
  }
1003
+ /**
1004
+ * Marks a goal as complete.
1005
+ *
1006
+ * @param id - The unique identifier of the goal to complete.
1007
+ * @param requestId - Optional custom identifier for the request.
1008
+ * @returns A promise that resolves to the updated goal.
1009
+ */
963
1010
  async completeGoal(id, requestId) {
964
1011
  return this.goalsClient.completeGoal(id, requestId);
965
1012
  }
1013
+ /**
1014
+ * Marks a previously completed goal as incomplete.
1015
+ *
1016
+ * @param id - The unique identifier of the goal to uncomplete.
1017
+ * @param requestId - Optional custom identifier for the request.
1018
+ * @returns A promise that resolves to the updated goal.
1019
+ */
966
1020
  async uncompleteGoal(id, requestId) {
967
1021
  return this.goalsClient.uncompleteGoal(id, requestId);
968
1022
  }
1023
+ /**
1024
+ * Links a task to a goal so it contributes to goal progress.
1025
+ *
1026
+ * @param args - The goal ID and task ID to link.
1027
+ * @param requestId - Optional custom identifier for the request.
1028
+ * @returns A promise that resolves to the updated goal.
1029
+ */
969
1030
  async linkTaskToGoal(args, requestId) {
970
1031
  return this.goalsClient.linkTaskToGoal(args, requestId);
971
1032
  }
1033
+ /**
1034
+ * Unlinks a previously linked task from a goal.
1035
+ *
1036
+ * @param args - The goal ID and task ID to unlink.
1037
+ * @param requestId - Optional custom identifier for the request.
1038
+ * @returns A promise that resolves to `true` if successful.
1039
+ */
972
1040
  async unlinkTaskFromGoal(args, requestId) {
973
1041
  return this.goalsClient.unlinkTaskFromGoal(args, requestId);
974
1042
  }
@@ -89,6 +89,7 @@ function convertResponseToCustomFetch(response) {
89
89
  headers: headersToObject(response.headers),
90
90
  text: () => clonedResponse.text(),
91
91
  json: () => response.json(),
92
+ arrayBuffer: () => response.arrayBuffer(),
92
93
  };
93
94
  }
94
95
  /**
@@ -0,0 +1,29 @@
1
+ import { headersToRecord } from './headers.js';
2
+ /**
3
+ * Wraps either a native `Response` or a `CustomFetchResponse` into the SDK's
4
+ * `FileResponse` shape (binary endpoints: `viewAttachment`, `downloadBackup`).
5
+ *
6
+ * `arrayBuffer()` is resolved lazily. A customFetch implementation that omits
7
+ * `arrayBuffer()` is still valid for callers that only read `text()`/`json()`;
8
+ * the descriptive error is thrown only if the caller actually requests binary
9
+ * bytes. This avoids the lossy `text()` → `TextEncoder` round-trip that
10
+ * corrupts non-UTF-8 bytes (every `0x80`-`0xFF` byte not part of a valid UTF-8
11
+ * sequence is replaced with `U+FFFD`).
12
+ */
13
+ export function wrapAsFileResponse(response, context) {
14
+ const headers = response.headers instanceof Headers ? headersToRecord(response.headers) : response.headers;
15
+ return {
16
+ ok: response.ok,
17
+ status: response.status,
18
+ statusText: response.statusText,
19
+ headers,
20
+ text: () => response.text(),
21
+ json: () => response.json(),
22
+ arrayBuffer: () => {
23
+ if (typeof response.arrayBuffer !== 'function') {
24
+ throw new Error(`customFetch response must implement arrayBuffer() for ${context} (binary endpoint); reading the body via text() corrupts non-UTF-8 bytes`);
25
+ }
26
+ return response.arrayBuffer();
27
+ },
28
+ };
29
+ }
@@ -1,4 +1,5 @@
1
1
  import { fetchWithRetry } from '../transport/fetch-with-retry.js';
2
+ import { camelCaseKeys } from './case-conversion.js';
2
3
  /**
3
4
  * Helper function to determine content-type from filename extension.
4
5
  * @param fileName - The filename to analyze
@@ -138,5 +139,5 @@ export async function uploadMultipartFile(args) {
138
139
  },
139
140
  customFetch,
140
141
  });
141
- return response.data;
142
+ return camelCaseKeys(response.data);
142
143
  }
@@ -789,15 +789,83 @@ export declare class TodoistApi {
789
789
  * @returns A promise that resolves to `true` if successful.
790
790
  */
791
791
  deleteFolder(id: string, requestId?: string): Promise<boolean>;
792
+ /**
793
+ * Retrieves a paginated list of goals.
794
+ *
795
+ * @param args - Optional filters including owner type, workspace ID, cursor, and limit.
796
+ * @returns A promise that resolves to a paginated list of goals.
797
+ */
792
798
  getGoals(args?: GetGoalsArgs): Promise<GetGoalsResponse>;
799
+ /**
800
+ * Searches for goals matching a query.
801
+ *
802
+ * @param args - Search parameters including query string and optional owner type.
803
+ * @returns A promise that resolves to a paginated list of matching goals.
804
+ */
793
805
  searchGoals(args: SearchGoalsArgs): Promise<GetGoalsResponse>;
806
+ /**
807
+ * Retrieves a single goal by its ID.
808
+ *
809
+ * @param id - The unique identifier of the goal.
810
+ * @returns A promise that resolves to the goal.
811
+ */
794
812
  getGoal(id: string): Promise<Goal>;
813
+ /**
814
+ * Creates a new goal.
815
+ *
816
+ * @param args - Goal attributes including name and optional workspace, description, deadline, and responsible user.
817
+ * @param requestId - Optional custom identifier for the request.
818
+ * @returns A promise that resolves to the created goal.
819
+ */
795
820
  addGoal(args: AddGoalArgs, requestId?: string): Promise<Goal>;
821
+ /**
822
+ * Updates an existing goal.
823
+ *
824
+ * @param id - The unique identifier of the goal to update.
825
+ * @param args - Attributes to update on the goal.
826
+ * @param requestId - Optional custom identifier for the request.
827
+ * @returns A promise that resolves to the updated goal.
828
+ */
796
829
  updateGoal(id: string, args: UpdateGoalArgs, requestId?: string): Promise<Goal>;
830
+ /**
831
+ * Deletes a goal by its ID.
832
+ *
833
+ * @param id - The unique identifier of the goal to delete.
834
+ * @param requestId - Optional custom identifier for the request.
835
+ * @returns A promise that resolves to `true` if successful.
836
+ */
797
837
  deleteGoal(id: string, requestId?: string): Promise<boolean>;
838
+ /**
839
+ * Marks a goal as complete.
840
+ *
841
+ * @param id - The unique identifier of the goal to complete.
842
+ * @param requestId - Optional custom identifier for the request.
843
+ * @returns A promise that resolves to the updated goal.
844
+ */
798
845
  completeGoal(id: string, requestId?: string): Promise<Goal>;
846
+ /**
847
+ * Marks a previously completed goal as incomplete.
848
+ *
849
+ * @param id - The unique identifier of the goal to uncomplete.
850
+ * @param requestId - Optional custom identifier for the request.
851
+ * @returns A promise that resolves to the updated goal.
852
+ */
799
853
  uncompleteGoal(id: string, requestId?: string): Promise<Goal>;
854
+ /**
855
+ * Links a task to a goal so it contributes to goal progress.
856
+ *
857
+ * @param args - The goal ID and task ID to link.
858
+ * @param requestId - Optional custom identifier for the request.
859
+ * @returns A promise that resolves to the updated goal.
860
+ */
800
861
  linkTaskToGoal(args: TaskLinkingArgs, requestId?: string): Promise<Goal>;
862
+ /**
863
+ * Unlinks a previously linked task from a goal.
864
+ *
865
+ * @param args - The goal ID and task ID to unlink.
866
+ * @param requestId - Optional custom identifier for the request.
867
+ * @returns A promise that resolves to `true` if successful.
868
+ */
801
869
  unlinkTaskFromGoal(args: TaskLinkingArgs, requestId?: string): Promise<boolean>;
802
870
  /**
803
871
  * Retrieves a list of available backups.
@@ -67,7 +67,12 @@ export declare function isNetworkError(error: Error): error is NetworkError;
67
67
  */
68
68
  export declare function isHttpError(error: Error): error is HttpError;
69
69
  /**
70
- * Custom fetch response interface that custom HTTP clients must implement
70
+ * Custom fetch response interface that custom HTTP clients must implement.
71
+ *
72
+ * `arrayBuffer()` is optional for back-compat with existing implementations,
73
+ * but must be provided when the fetch is used with binary endpoints
74
+ * (`viewAttachment`, `downloadBackup`) — those endpoints throw at runtime if
75
+ * it is missing rather than fall back to a lossy `text()` round-trip.
71
76
  */
72
77
  export type CustomFetchResponse = {
73
78
  ok: boolean;
@@ -76,6 +81,7 @@ export type CustomFetchResponse = {
76
81
  headers: Record<string, string>;
77
82
  text(): Promise<string>;
78
83
  json(): Promise<unknown>;
84
+ arrayBuffer?: () => Promise<ArrayBuffer>;
79
85
  };
80
86
  /**
81
87
  * Custom fetch function type for alternative HTTP clients
@@ -0,0 +1,15 @@
1
+ import type { CustomFetchResponse, FileResponse } from '../types/http.js';
2
+ type AnyResponse = CustomFetchResponse | Response;
3
+ /**
4
+ * Wraps either a native `Response` or a `CustomFetchResponse` into the SDK's
5
+ * `FileResponse` shape (binary endpoints: `viewAttachment`, `downloadBackup`).
6
+ *
7
+ * `arrayBuffer()` is resolved lazily. A customFetch implementation that omits
8
+ * `arrayBuffer()` is still valid for callers that only read `text()`/`json()`;
9
+ * the descriptive error is thrown only if the caller actually requests binary
10
+ * bytes. This avoids the lossy `text()` → `TextEncoder` round-trip that
11
+ * corrupts non-UTF-8 bytes (every `0x80`-`0xFF` byte not part of a valid UTF-8
12
+ * sequence is replaced with `U+FFFD`).
13
+ */
14
+ export declare function wrapAsFileResponse(response: AnyResponse, context: 'viewAttachment' | 'downloadBackup'): FileResponse;
15
+ export {};
@@ -50,5 +50,5 @@ type UploadMultipartFileArgs = {
50
50
  * )
51
51
  * ```
52
52
  */
53
- export declare function uploadMultipartFile<T>(args: UploadMultipartFileArgs): Promise<T>;
53
+ export declare function uploadMultipartFile(args: UploadMultipartFileArgs): Promise<unknown>;
54
54
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-sdk",
3
- "version": "10.2.0-next.2",
3
+ "version": "10.2.0-next.4",
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",
@@ -41,11 +41,14 @@
41
41
  "build:fix-dts": "node scripts/fix-dts-imports.cjs",
42
42
  "build:post": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
43
43
  "build": "npm-run-all clean build:cjs build:esm build:fix-esm build:fix-dts build:post",
44
- "attw": "npx @arethetypeswrong/cli --pack --ignore-rules fallback-condition false-esm",
44
+ "attw": "attw --pack --ignore-rules fallback-condition false-esm",
45
45
  "integrity-checks": "npm-run-all clean check test build attw",
46
46
  "prepublishOnly": "npm run integrity-checks",
47
47
  "prepare": "npm run build"
48
48
  },
49
+ "overrides": {
50
+ "fflate": "0.8.2"
51
+ },
49
52
  "dependencies": {
50
53
  "camelcase": "6.3.0",
51
54
  "emoji-regex": "10.6.0",
@@ -56,6 +59,7 @@
56
59
  "zod": "4.3.6"
57
60
  },
58
61
  "devDependencies": {
62
+ "@arethetypeswrong/cli": "0.18.2",
59
63
  "@semantic-release/changelog": "6.0.3",
60
64
  "@semantic-release/git": "10.0.1",
61
65
  "conventional-changelog-conventionalcommits": "9.3.1",