@todoist/sdk 0.0.1 → 18.0.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.
- package/LICENSE +21 -0
- package/README.md +293 -29
- package/dist/esm/authentication.js +307 -0
- package/dist/esm/clients/activity-client.js +60 -0
- package/dist/esm/clients/app-client.js +366 -0
- package/dist/esm/clients/backup-client.js +47 -0
- package/dist/esm/clients/base-client.js +29 -0
- package/dist/esm/clients/billing-client.js +160 -0
- package/dist/esm/clients/comment-client.js +92 -0
- package/dist/esm/clients/email-client.js +41 -0
- package/dist/esm/clients/filter-client.js +26 -0
- package/dist/esm/clients/folder-client.js +80 -0
- package/dist/esm/clients/id-mapping-client.js +37 -0
- package/dist/esm/clients/insights-client.js +89 -0
- package/dist/esm/clients/label-client.js +131 -0
- package/dist/esm/clients/productivity-client.js +24 -0
- package/dist/esm/clients/project-client.js +284 -0
- package/dist/esm/clients/reminder-client.js +204 -0
- package/dist/esm/clients/section-client.js +121 -0
- package/dist/esm/clients/task-client.js +264 -0
- package/dist/esm/clients/template-client.js +89 -0
- package/dist/esm/clients/ui-extension-client.js +110 -0
- package/dist/esm/clients/upload-client.js +153 -0
- package/dist/esm/clients/view-options-client.js +21 -0
- package/dist/esm/clients/workspace-client.js +384 -0
- package/dist/esm/consts/endpoints.js +193 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/test-utils/asserts.js +8 -0
- package/dist/esm/test-utils/msw-setup.js +88 -0
- package/dist/esm/test-utils/obsidian-fetch-adapter.js +51 -0
- package/dist/esm/test-utils/test-defaults.js +367 -0
- package/dist/esm/test-utils/transport-seam.js +15 -0
- package/dist/esm/test-utils/webhook-fixtures.js +26 -0
- package/dist/esm/todoist-api.js +1757 -0
- package/dist/esm/transport/fetch-with-retry.js +216 -0
- package/dist/esm/transport/http-client.js +140 -0
- package/dist/esm/transport/http-dispatcher.js +158 -0
- package/dist/esm/transport/sync-request.js +106 -0
- package/dist/esm/types/activity/index.js +2 -0
- package/dist/esm/types/activity/requests.js +1 -0
- package/dist/esm/types/activity/types.js +45 -0
- package/dist/esm/types/apps/index.js +2 -0
- package/dist/esm/types/apps/requests.js +1 -0
- package/dist/esm/types/apps/types.js +249 -0
- package/dist/esm/types/backups/index.js +2 -0
- package/dist/esm/types/backups/requests.js +1 -0
- package/dist/esm/types/backups/types.js +5 -0
- package/dist/esm/types/billing/index.js +2 -0
- package/dist/esm/types/billing/requests.js +1 -0
- package/dist/esm/types/billing/types.js +89 -0
- package/dist/esm/types/comments/index.js +2 -0
- package/dist/esm/types/comments/requests.js +1 -0
- package/dist/esm/types/comments/types.js +57 -0
- package/dist/esm/types/common.js +7 -0
- package/dist/esm/types/emails/index.js +1 -0
- package/dist/esm/types/emails/requests.js +2 -0
- package/dist/esm/types/errors.js +41 -0
- package/dist/esm/types/folders/index.js +1 -0
- package/dist/esm/types/folders/requests.js +1 -0
- package/dist/esm/types/http.js +20 -0
- package/dist/esm/types/id-mappings/index.js +2 -0
- package/dist/esm/types/id-mappings/requests.js +17 -0
- package/dist/esm/types/id-mappings/types.js +9 -0
- package/dist/esm/types/index.js +24 -0
- package/dist/esm/types/insights/index.js +2 -0
- package/dist/esm/types/insights/requests.js +1 -0
- package/dist/esm/types/insights/types.js +77 -0
- package/dist/esm/types/labels/index.js +2 -0
- package/dist/esm/types/labels/requests.js +1 -0
- package/dist/esm/types/labels/types.js +8 -0
- package/dist/esm/types/productivity/index.js +1 -0
- package/dist/esm/types/productivity/types.js +65 -0
- package/dist/esm/types/projects/index.js +2 -0
- package/dist/esm/types/projects/requests.js +1 -0
- package/dist/esm/types/projects/types.js +65 -0
- package/dist/esm/types/reminders/index.js +1 -0
- package/dist/esm/types/reminders/requests.js +92 -0
- package/dist/esm/types/sections/index.js +2 -0
- package/dist/esm/types/sections/requests.js +1 -0
- package/dist/esm/types/sections/types.js +23 -0
- package/dist/esm/types/sync/commands/calendars.js +1 -0
- package/dist/esm/types/sync/commands/filters.js +1 -0
- package/dist/esm/types/sync/commands/folders.js +1 -0
- package/dist/esm/types/sync/commands/index.js +18 -0
- package/dist/esm/types/sync/commands/labels.js +2 -0
- package/dist/esm/types/sync/commands/notes.js +1 -0
- package/dist/esm/types/sync/commands/notifications.js +1 -0
- package/dist/esm/types/sync/commands/others.js +1 -0
- package/dist/esm/types/sync/commands/project-view-options.js +1 -0
- package/dist/esm/types/sync/commands/projects.js +1 -0
- package/dist/esm/types/sync/commands/reminders.js +1 -0
- package/dist/esm/types/sync/commands/sections.js +1 -0
- package/dist/esm/types/sync/commands/shared.js +16 -0
- package/dist/esm/types/sync/commands/sharing.js +1 -0
- package/dist/esm/types/sync/commands/tasks.js +1 -0
- package/dist/esm/types/sync/commands/view-options.js +1 -0
- package/dist/esm/types/sync/commands/workspace-filters.js +1 -0
- package/dist/esm/types/sync/commands/workspaces.js +1 -0
- package/dist/esm/types/sync/index.js +6 -0
- package/dist/esm/types/sync/request.js +1 -0
- package/dist/esm/types/sync/resource-types.js +36 -0
- package/dist/esm/types/sync/resources/calendars.js +24 -0
- package/dist/esm/types/sync/resources/collaborators.js +21 -0
- package/dist/esm/types/sync/resources/completed-info.js +21 -0
- package/dist/esm/types/sync/resources/filters.js +12 -0
- package/dist/esm/types/sync/resources/folders.js +10 -0
- package/dist/esm/types/sync/resources/index.js +18 -0
- package/dist/esm/types/sync/resources/labels.js +7 -0
- package/dist/esm/types/sync/resources/live-notifications.js +35 -0
- package/dist/esm/types/sync/resources/notes.js +18 -0
- package/dist/esm/types/sync/resources/reminders.js +58 -0
- package/dist/esm/types/sync/resources/suggestions.js +33 -0
- package/dist/esm/types/sync/resources/tooltips.js +5 -0
- package/dist/esm/types/sync/resources/user-plan-limits.js +21 -0
- package/dist/esm/types/sync/resources/user-settings.js +41 -0
- package/dist/esm/types/sync/resources/user-status.js +22 -0
- package/dist/esm/types/sync/resources/user.js +109 -0
- package/dist/esm/types/sync/resources/view-options.js +128 -0
- package/dist/esm/types/sync/resources/workspace-filters.js +17 -0
- package/dist/esm/types/sync/resources/workspaces.js +69 -0
- package/dist/esm/types/sync/response.js +57 -0
- package/dist/esm/types/sync/user-preferences.js +68 -0
- package/dist/esm/types/tasks/index.js +2 -0
- package/dist/esm/types/tasks/requests.js +1 -0
- package/dist/esm/types/tasks/types.js +56 -0
- package/dist/esm/types/templates/index.js +1 -0
- package/dist/esm/types/templates/requests.js +1 -0
- package/dist/esm/types/ui-extensions/index.js +2 -0
- package/dist/esm/types/ui-extensions/requests.js +1 -0
- package/dist/esm/types/ui-extensions/types.js +74 -0
- package/dist/esm/types/uploads/index.js +1 -0
- package/dist/esm/types/uploads/requests.js +1 -0
- package/dist/esm/types/users/index.js +1 -0
- package/dist/esm/types/users/types.js +49 -0
- package/dist/esm/types/webhooks/comments.js +93 -0
- package/dist/esm/types/webhooks/envelope.js +23 -0
- package/dist/esm/types/webhooks/filters.js +27 -0
- package/dist/esm/types/webhooks/index.js +9 -0
- package/dist/esm/types/webhooks/labels.js +51 -0
- package/dist/esm/types/webhooks/projects.js +33 -0
- package/dist/esm/types/webhooks/reminders.js +32 -0
- package/dist/esm/types/webhooks/sections.js +50 -0
- package/dist/esm/types/webhooks/tasks.js +65 -0
- package/dist/esm/types/webhooks/types.js +52 -0
- package/dist/esm/types/workspaces/index.js +2 -0
- package/dist/esm/types/workspaces/requests.js +1 -0
- package/dist/esm/types/workspaces/types.js +151 -0
- package/dist/esm/utils/activity-helpers.js +53 -0
- package/dist/esm/utils/case-conversion.js +84 -0
- package/dist/esm/utils/colors.js +145 -0
- package/dist/esm/utils/file-response.js +29 -0
- package/dist/esm/utils/filter-queries.js +231 -0
- package/dist/esm/utils/filter-query-definitions.js +2789 -0
- package/dist/esm/utils/headers.js +42 -0
- package/dist/esm/utils/index.js +11 -0
- package/dist/esm/utils/multipart-upload.js +267 -0
- package/dist/esm/utils/person-names.js +61 -0
- package/dist/esm/utils/processing-helpers.js +12 -0
- package/dist/esm/utils/project-helpers.js +29 -0
- package/dist/esm/utils/request-helpers.js +43 -0
- package/dist/esm/utils/sanitization.js +131 -0
- package/dist/esm/utils/sync-helpers.js +19 -0
- package/dist/esm/utils/task-sorting.js +280 -0
- package/dist/esm/utils/uncompletable-helpers.js +54 -0
- package/dist/esm/utils/url-helpers.js +196 -0
- package/dist/esm/utils/validators.js +102 -0
- package/dist/esm/utils/view-options.js +15 -0
- package/dist/esm/utils/webhook-parser.js +56 -0
- package/dist/types/authentication.d.ts +262 -0
- package/dist/types/clients/activity-client.d.ts +12 -0
- package/dist/types/clients/app-client.d.ts +37 -0
- package/dist/types/clients/backup-client.d.ts +14 -0
- package/dist/types/clients/base-client.d.ts +23 -0
- package/dist/types/clients/billing-client.d.ts +22 -0
- package/dist/types/clients/comment-client.d.ts +15 -0
- package/dist/types/clients/email-client.d.ts +13 -0
- package/dist/types/clients/filter-client.d.ts +10 -0
- package/dist/types/clients/folder-client.d.ts +16 -0
- package/dist/types/clients/id-mapping-client.d.ts +13 -0
- package/dist/types/clients/insights-client.d.ts +18 -0
- package/dist/types/clients/label-client.d.ts +19 -0
- package/dist/types/clients/productivity-client.d.ts +12 -0
- package/dist/types/clients/project-client.d.ts +22 -0
- package/dist/types/clients/reminder-client.d.ts +25 -0
- package/dist/types/clients/section-client.d.ts +18 -0
- package/dist/types/clients/task-client.d.ts +25 -0
- package/dist/types/clients/template-client.d.ts +17 -0
- package/dist/types/clients/ui-extension-client.d.ts +18 -0
- package/dist/types/clients/upload-client.d.ts +17 -0
- package/dist/types/clients/view-options-client.d.ts +8 -0
- package/dist/types/clients/workspace-client.d.ts +43 -0
- package/dist/types/consts/endpoints.d.ts +120 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/test-utils/asserts.d.ts +1 -0
- package/dist/types/test-utils/msw-setup.d.ts +33 -0
- package/dist/types/test-utils/obsidian-fetch-adapter.d.ts +29 -0
- package/dist/types/test-utils/test-defaults.d.ts +286 -0
- package/dist/types/test-utils/transport-seam.d.ts +1 -0
- package/dist/types/test-utils/webhook-fixtures.d.ts +13 -0
- package/dist/types/todoist-api.d.ts +1437 -0
- package/dist/types/transport/fetch-with-retry.d.ts +12 -0
- package/dist/types/transport/http-client.d.ts +25 -0
- package/dist/types/transport/http-dispatcher.d.ts +65 -0
- package/dist/types/transport/sync-request.d.ts +31 -0
- package/dist/types/types/activity/index.d.ts +2 -0
- package/dist/types/types/activity/requests.d.ts +85 -0
- package/dist/types/types/activity/types.d.ts +56 -0
- package/dist/types/types/apps/index.d.ts +2 -0
- package/dist/types/types/apps/requests.d.ts +89 -0
- package/dist/types/types/apps/types.d.ts +418 -0
- package/dist/types/types/backups/index.d.ts +2 -0
- package/dist/types/types/backups/requests.d.ts +16 -0
- package/dist/types/types/backups/types.d.ts +7 -0
- package/dist/types/types/billing/index.d.ts +2 -0
- package/dist/types/types/billing/requests.d.ts +62 -0
- package/dist/types/types/billing/types.d.ts +207 -0
- package/dist/types/types/comments/index.d.ts +2 -0
- package/dist/types/types/comments/requests.d.ts +64 -0
- package/dist/types/types/comments/types.d.ts +141 -0
- package/dist/types/types/common.d.ts +13 -0
- package/dist/types/types/emails/index.d.ts +1 -0
- package/dist/types/types/emails/requests.d.ts +30 -0
- package/dist/types/types/errors.d.ts +29 -0
- package/dist/types/types/folders/index.d.ts +1 -0
- package/dist/types/types/folders/requests.d.ts +20 -0
- package/dist/types/types/http.d.ts +104 -0
- package/dist/types/types/id-mappings/index.d.ts +2 -0
- package/dist/types/types/id-mappings/requests.d.ts +28 -0
- package/dist/types/types/id-mappings/types.d.ts +13 -0
- package/dist/types/types/index.d.ts +24 -0
- package/dist/types/types/insights/index.d.ts +2 -0
- package/dist/types/types/insights/requests.d.ts +26 -0
- package/dist/types/types/insights/types.d.ts +167 -0
- package/dist/types/types/labels/index.d.ts +2 -0
- package/dist/types/types/labels/requests.d.ts +75 -0
- package/dist/types/types/labels/types.d.ts +13 -0
- package/dist/types/types/productivity/index.d.ts +1 -0
- package/dist/types/types/productivity/types.d.ts +115 -0
- package/dist/types/types/projects/index.d.ts +2 -0
- package/dist/types/types/projects/requests.d.ts +206 -0
- package/dist/types/types/projects/types.d.ts +286 -0
- package/dist/types/types/reminders/index.d.ts +1 -0
- package/dist/types/types/reminders/requests.d.ts +205 -0
- package/dist/types/types/sections/index.d.ts +2 -0
- package/dist/types/types/sections/requests.d.ts +49 -0
- package/dist/types/types/sections/types.d.ts +70 -0
- package/dist/types/types/sync/commands/calendars.d.ts +14 -0
- package/dist/types/types/sync/commands/filters.d.ts +24 -0
- package/dist/types/types/sync/commands/folders.d.ts +20 -0
- package/dist/types/types/sync/commands/index.d.ts +167 -0
- package/dist/types/types/sync/commands/labels.d.ts +32 -0
- package/dist/types/types/sync/commands/notes.d.ts +27 -0
- package/dist/types/types/sync/commands/notifications.d.ts +10 -0
- package/dist/types/types/sync/commands/others.d.ts +61 -0
- package/dist/types/types/sync/commands/project-view-options.d.ts +13 -0
- package/dist/types/types/sync/commands/projects.d.ts +96 -0
- package/dist/types/types/sync/commands/reminders.d.ts +51 -0
- package/dist/types/types/sync/commands/sections.d.ts +36 -0
- package/dist/types/types/sync/commands/shared.d.ts +51 -0
- package/dist/types/types/sync/commands/sharing.d.ts +16 -0
- package/dist/types/types/sync/commands/tasks.d.ts +78 -0
- package/dist/types/types/sync/commands/view-options.d.ts +18 -0
- package/dist/types/types/sync/commands/workspace-filters.d.ts +25 -0
- package/dist/types/types/sync/commands/workspaces.d.ts +82 -0
- package/dist/types/types/sync/index.d.ts +6 -0
- package/dist/types/types/sync/request.d.ts +7 -0
- package/dist/types/types/sync/resource-types.d.ts +5 -0
- package/dist/types/types/sync/resources/calendars.d.ts +38 -0
- package/dist/types/types/sync/resources/collaborators.d.ts +45 -0
- package/dist/types/types/sync/resources/completed-info.d.ts +33 -0
- package/dist/types/types/sync/resources/filters.d.ts +13 -0
- package/dist/types/types/sync/resources/folders.d.ts +11 -0
- package/dist/types/types/sync/resources/index.d.ts +18 -0
- package/dist/types/types/sync/resources/labels.d.ts +12 -0
- package/dist/types/types/sync/resources/live-notifications.d.ts +32 -0
- package/dist/types/types/sync/resources/notes.d.ts +34 -0
- package/dist/types/types/sync/resources/reminders.d.ts +213 -0
- package/dist/types/types/sync/resources/suggestions.d.ts +96 -0
- package/dist/types/types/sync/resources/tooltips.d.ts +6 -0
- package/dist/types/types/sync/resources/user-plan-limits.d.ts +47 -0
- package/dist/types/types/sync/resources/user-settings.d.ts +40 -0
- package/dist/types/types/sync/resources/user-status.d.ts +46 -0
- package/dist/types/types/sync/resources/user.d.ts +151 -0
- package/dist/types/types/sync/resources/view-options.d.ts +157 -0
- package/dist/types/types/sync/resources/workspace-filters.d.ts +18 -0
- package/dist/types/types/sync/resources/workspaces.d.ts +142 -0
- package/dist/types/types/sync/response.d.ts +930 -0
- package/dist/types/types/sync/user-preferences.d.ts +39 -0
- package/dist/types/types/tasks/index.d.ts +2 -0
- package/dist/types/types/tasks/requests.d.ts +197 -0
- package/dist/types/types/tasks/types.d.ts +162 -0
- package/dist/types/types/templates/index.d.ts +1 -0
- package/dist/types/types/templates/requests.d.ts +92 -0
- package/dist/types/types/ui-extensions/index.d.ts +2 -0
- package/dist/types/types/ui-extensions/requests.d.ts +108 -0
- package/dist/types/types/ui-extensions/types.d.ts +185 -0
- package/dist/types/types/uploads/index.d.ts +1 -0
- package/dist/types/types/uploads/requests.d.ts +32 -0
- package/dist/types/types/users/index.d.ts +1 -0
- package/dist/types/types/users/types.d.ts +67 -0
- package/dist/types/types/webhooks/comments.d.ts +1600 -0
- package/dist/types/types/webhooks/envelope.d.ts +34 -0
- package/dist/types/types/webhooks/filters.d.ts +99 -0
- package/dist/types/types/webhooks/index.d.ts +9 -0
- package/dist/types/types/webhooks/labels.d.ts +160 -0
- package/dist/types/types/webhooks/projects.d.ts +365 -0
- package/dist/types/types/webhooks/reminders.d.ts +82 -0
- package/dist/types/types/webhooks/sections.d.ts +372 -0
- package/dist/types/types/webhooks/tasks.d.ts +981 -0
- package/dist/types/types/webhooks/types.d.ts +2606 -0
- package/dist/types/types/workspaces/index.d.ts +2 -0
- package/dist/types/types/workspaces/requests.d.ts +260 -0
- package/dist/types/types/workspaces/types.d.ts +266 -0
- package/dist/types/utils/activity-helpers.d.ts +28 -0
- package/dist/types/utils/case-conversion.d.ts +20 -0
- package/dist/types/utils/colors.d.ts +201 -0
- package/dist/types/utils/file-response.d.ts +15 -0
- package/dist/types/utils/filter-queries.d.ts +42 -0
- package/dist/types/utils/filter-query-definitions.d.ts +27 -0
- package/dist/types/utils/headers.d.ts +20 -0
- package/dist/types/utils/index.d.ts +15 -0
- package/dist/types/utils/multipart-upload.d.ts +58 -0
- package/dist/types/utils/person-names.d.ts +52 -0
- package/dist/types/utils/processing-helpers.d.ts +1 -0
- package/dist/types/utils/project-helpers.d.ts +21 -0
- package/dist/types/utils/request-helpers.d.ts +33 -0
- package/dist/types/utils/sanitization.d.ts +52 -0
- package/dist/types/utils/sync-helpers.d.ts +17 -0
- package/dist/types/utils/task-sorting.d.ts +46 -0
- package/dist/types/utils/uncompletable-helpers.d.ts +30 -0
- package/dist/types/utils/url-helpers.d.ts +120 -0
- package/dist/types/utils/validators.d.ts +1917 -0
- package/dist/types/utils/view-options.d.ts +11 -0
- package/dist/types/utils/webhook-parser.d.ts +48 -0
- package/package.json +87 -8
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { isNetworkError } from '../types/http.js';
|
|
13
|
+
import { getDefaultTransport } from './http-dispatcher.js';
|
|
14
|
+
/**
|
|
15
|
+
* Default retry configuration matching the original axios-retry behavior
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
18
|
+
retries: 3,
|
|
19
|
+
retryCondition: isNetworkError,
|
|
20
|
+
retryDelay: (retryNumber) => {
|
|
21
|
+
// First retry: immediate (0ms delay)
|
|
22
|
+
// Subsequent retries: 500ms delay
|
|
23
|
+
return retryNumber === 1 ? 0 : 500;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const TIMEOUT_ERROR_NAME = 'TimeoutError';
|
|
27
|
+
function createTimeoutError(timeoutMs) {
|
|
28
|
+
const error = new Error(`Request timeout after ${timeoutMs}ms`);
|
|
29
|
+
error.name = TIMEOUT_ERROR_NAME;
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Converts Headers object to a plain object
|
|
34
|
+
*/
|
|
35
|
+
function headersToObject(headers) {
|
|
36
|
+
const result = {};
|
|
37
|
+
headers.forEach((value, key) => {
|
|
38
|
+
result[key] = value;
|
|
39
|
+
});
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates an AbortSignal that aborts after timeoutMs. Returns the signal and a
|
|
44
|
+
* clear function to cancel the timeout early.
|
|
45
|
+
*/
|
|
46
|
+
function createTimeoutSignal(timeoutMs, existingSignal) {
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
// Timeout logic
|
|
49
|
+
const timeoutId = setTimeout(() => {
|
|
50
|
+
controller.abort(createTimeoutError(timeoutMs));
|
|
51
|
+
}, timeoutMs);
|
|
52
|
+
let abortHandler;
|
|
53
|
+
function clear() {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
if (existingSignal && abortHandler) {
|
|
56
|
+
existingSignal.removeEventListener('abort', abortHandler);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// If there's an existing signal, forward its abort
|
|
60
|
+
if (existingSignal) {
|
|
61
|
+
if (existingSignal.aborted) {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
controller.abort(existingSignal.reason);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
abortHandler = () => {
|
|
67
|
+
clearTimeout(timeoutId);
|
|
68
|
+
controller.abort(existingSignal.reason);
|
|
69
|
+
};
|
|
70
|
+
existingSignal.addEventListener('abort', abortHandler, { once: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Clean up timeout when request completes
|
|
74
|
+
controller.signal.addEventListener('abort', () => {
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
});
|
|
77
|
+
return { signal: controller.signal, clear };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Converts native fetch Response to CustomFetchResponse for consistency
|
|
81
|
+
*/
|
|
82
|
+
function convertResponseToCustomFetch(response) {
|
|
83
|
+
// Clone the response so we can read it multiple times (if clone method exists)
|
|
84
|
+
const clonedResponse = response.clone ? response.clone() : response;
|
|
85
|
+
return {
|
|
86
|
+
ok: response.ok,
|
|
87
|
+
status: response.status,
|
|
88
|
+
statusText: response.statusText,
|
|
89
|
+
headers: headersToObject(response.headers),
|
|
90
|
+
text: () => clonedResponse.text(),
|
|
91
|
+
json: () => response.json(),
|
|
92
|
+
arrayBuffer: () => response.arrayBuffer(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Performs a fetch request with retry logic and timeout support
|
|
97
|
+
*/
|
|
98
|
+
export async function fetchWithRetry(args) {
|
|
99
|
+
var _a;
|
|
100
|
+
const { url, options = {}, retryConfig = {}, customFetch } = args;
|
|
101
|
+
const config = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), retryConfig);
|
|
102
|
+
const { timeout, signal: userSignal } = options, fetchOptions = __rest(options, ["timeout", "signal"]);
|
|
103
|
+
let lastError;
|
|
104
|
+
for (let attempt = 0; attempt <= config.retries; attempt++) {
|
|
105
|
+
// Timeout clear function for this attempt (hoisted for catch scope)
|
|
106
|
+
let clearTimeoutFn;
|
|
107
|
+
try {
|
|
108
|
+
// Set up timeout and signal handling
|
|
109
|
+
let requestSignal = userSignal || undefined;
|
|
110
|
+
if (timeout && timeout > 0) {
|
|
111
|
+
const timeoutResult = createTimeoutSignal(timeout, requestSignal);
|
|
112
|
+
requestSignal = timeoutResult.signal;
|
|
113
|
+
clearTimeoutFn = timeoutResult.clear;
|
|
114
|
+
}
|
|
115
|
+
// Use custom fetch or native fetch
|
|
116
|
+
let fetchResponse;
|
|
117
|
+
if (customFetch) {
|
|
118
|
+
fetchResponse = await customFetch(url, Object.assign(Object.assign({}, fetchOptions), { signal: requestSignal, timeout }));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Read the dispatcher and its paired `fetch` as one value so
|
|
122
|
+
// they can never be mismatched. On Node, `fetch` is undici's
|
|
123
|
+
// own — paired with the dispatcher — so the request client and
|
|
124
|
+
// dispatcher stay on one undici version; otherwise the
|
|
125
|
+
// `decompress` interceptor terminates gzip responses when the
|
|
126
|
+
// global `fetch`'s bundled undici differs. Browser/edge (and
|
|
127
|
+
// Bun) have no paired `fetch` and fall back to the global one.
|
|
128
|
+
const transport = await getDefaultTransport();
|
|
129
|
+
const nativeFetchOptions = Object.assign(Object.assign({}, fetchOptions), { signal: requestSignal });
|
|
130
|
+
if ((transport === null || transport === void 0 ? void 0 : transport.dispatcher) !== undefined) {
|
|
131
|
+
// @ts-expect-error - dispatcher is a valid option for Node.js fetch but not in the TS types
|
|
132
|
+
nativeFetchOptions.dispatcher = transport.dispatcher;
|
|
133
|
+
}
|
|
134
|
+
// undici's `fetch` and the global `fetch` are the same function
|
|
135
|
+
// at runtime but carry different (undici vs DOM) types. Call
|
|
136
|
+
// through the global signature, which matches the options above.
|
|
137
|
+
const fetchImpl = ((_a = transport === null || transport === void 0 ? void 0 : transport.fetch) !== null && _a !== void 0 ? _a : fetch);
|
|
138
|
+
const nativeResponse = await fetchImpl(url, nativeFetchOptions);
|
|
139
|
+
fetchResponse = convertResponseToCustomFetch(nativeResponse);
|
|
140
|
+
}
|
|
141
|
+
// Check if the response is successful
|
|
142
|
+
if (!fetchResponse.ok) {
|
|
143
|
+
const errorMessage = `HTTP ${fetchResponse.status}: ${fetchResponse.statusText}`;
|
|
144
|
+
const error = new Error(errorMessage);
|
|
145
|
+
error.status = fetchResponse.status;
|
|
146
|
+
error.statusText = fetchResponse.statusText;
|
|
147
|
+
error.response = {
|
|
148
|
+
data: undefined, // Will be set below if we can parse the response
|
|
149
|
+
status: fetchResponse.status,
|
|
150
|
+
statusText: fetchResponse.statusText,
|
|
151
|
+
headers: fetchResponse.headers,
|
|
152
|
+
};
|
|
153
|
+
// Try to get response body for error details
|
|
154
|
+
try {
|
|
155
|
+
const responseText = await fetchResponse.text();
|
|
156
|
+
let responseData;
|
|
157
|
+
try {
|
|
158
|
+
responseData = responseText ? JSON.parse(responseText) : undefined;
|
|
159
|
+
}
|
|
160
|
+
catch (_b) {
|
|
161
|
+
responseData = responseText;
|
|
162
|
+
}
|
|
163
|
+
error.data = responseData;
|
|
164
|
+
error.response.data = responseData;
|
|
165
|
+
}
|
|
166
|
+
catch (_c) {
|
|
167
|
+
// If we can't read the response body, that's OK
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
// Parse response
|
|
172
|
+
const responseText = await fetchResponse.text();
|
|
173
|
+
let data;
|
|
174
|
+
try {
|
|
175
|
+
data = responseText ? JSON.parse(responseText) : undefined;
|
|
176
|
+
}
|
|
177
|
+
catch (_d) {
|
|
178
|
+
// If JSON parsing fails, return the raw text
|
|
179
|
+
data = responseText;
|
|
180
|
+
}
|
|
181
|
+
// Success – clear pending timeout (if any) so Node can exit promptly
|
|
182
|
+
if (clearTimeoutFn) {
|
|
183
|
+
clearTimeoutFn();
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
data,
|
|
187
|
+
status: fetchResponse.status,
|
|
188
|
+
statusText: fetchResponse.statusText,
|
|
189
|
+
headers: fetchResponse.headers,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (clearTimeoutFn) {
|
|
194
|
+
clearTimeoutFn();
|
|
195
|
+
}
|
|
196
|
+
lastError = error;
|
|
197
|
+
// Check if this error should trigger a retry
|
|
198
|
+
const shouldRetry = attempt < config.retries && config.retryCondition(lastError);
|
|
199
|
+
if (!shouldRetry) {
|
|
200
|
+
// Add network error flag for network errors
|
|
201
|
+
if (isNetworkError(lastError)) {
|
|
202
|
+
const networkError = lastError;
|
|
203
|
+
networkError.isNetworkError = true;
|
|
204
|
+
}
|
|
205
|
+
throw lastError;
|
|
206
|
+
}
|
|
207
|
+
// Wait before retrying
|
|
208
|
+
const delay = config.retryDelay(attempt + 1);
|
|
209
|
+
if (delay > 0) {
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// This should never be reached, but just in case
|
|
215
|
+
throw lastError || new Error('Request failed after retries');
|
|
216
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { API_BASE_URI } from '../consts/endpoints.js';
|
|
3
|
+
import { TodoistRequestError } from '../types/errors.js';
|
|
4
|
+
import { isHttpError, isNetworkError } from '../types/http.js';
|
|
5
|
+
import { camelCaseKeys, snakeCaseKeys } from '../utils/case-conversion.js';
|
|
6
|
+
import { mergeHeaders, resolveCustomHeaders } from '../utils/headers.js';
|
|
7
|
+
import { fetchWithRetry } from './fetch-with-retry.js';
|
|
8
|
+
export function paramsSerializer(params) {
|
|
9
|
+
const qs = new URLSearchParams();
|
|
10
|
+
Object.keys(params).forEach((key) => {
|
|
11
|
+
const value = params[key];
|
|
12
|
+
if (value !== null && value !== undefined) {
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
qs.append(key, JSON.stringify(value));
|
|
15
|
+
}
|
|
16
|
+
else if (typeof value === 'string' ||
|
|
17
|
+
typeof value === 'number' ||
|
|
18
|
+
typeof value === 'boolean') {
|
|
19
|
+
qs.append(key, String(value));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
qs.append(key, JSON.stringify(value));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return qs.toString();
|
|
27
|
+
}
|
|
28
|
+
const defaultHeaders = {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
};
|
|
31
|
+
function getAuthHeader(apiKey) {
|
|
32
|
+
return `Bearer ${apiKey}`;
|
|
33
|
+
}
|
|
34
|
+
function getRetryDelay(retryCount) {
|
|
35
|
+
return retryCount === 1 ? 0 : 500;
|
|
36
|
+
}
|
|
37
|
+
function getTodoistRequestError(args) {
|
|
38
|
+
const { error, originalStack } = args;
|
|
39
|
+
const requestError = new TodoistRequestError(error.message);
|
|
40
|
+
requestError.stack = originalStack ? originalStack.stack : error.stack;
|
|
41
|
+
if (isHttpError(error)) {
|
|
42
|
+
requestError.httpStatusCode = error.status;
|
|
43
|
+
requestError.responseData = error.data;
|
|
44
|
+
}
|
|
45
|
+
return requestError;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalises anything thrown while making an API request into the error a
|
|
49
|
+
* caller should see: a `TodoistRequestError` (carrying the HTTP status and
|
|
50
|
+
* response body when there is one) reported against `originalStack`, or a
|
|
51
|
+
* generic `Error` when the thrown value isn't an `Error` at all.
|
|
52
|
+
*/
|
|
53
|
+
export function toTodoistRequestError(error, originalStack) {
|
|
54
|
+
if (!(error instanceof Error)) {
|
|
55
|
+
return new Error('An unknown error occurred during the request');
|
|
56
|
+
}
|
|
57
|
+
return getTodoistRequestError({ error, originalStack });
|
|
58
|
+
}
|
|
59
|
+
function getRequestConfiguration(args) {
|
|
60
|
+
const { baseURL, apiToken, requestId, customHeaders } = args;
|
|
61
|
+
const authHeader = apiToken ? { Authorization: getAuthHeader(apiToken) } : undefined;
|
|
62
|
+
const requestIdHeader = requestId ? { 'X-Request-Id': requestId } : undefined;
|
|
63
|
+
// Custom headers sit below auth and request id so they can never replace them.
|
|
64
|
+
const headers = mergeHeaders(defaultHeaders, resolveCustomHeaders(customHeaders), authHeader, requestIdHeader);
|
|
65
|
+
return { baseURL, headers };
|
|
66
|
+
}
|
|
67
|
+
function getHttpClientConfig(args) {
|
|
68
|
+
const { baseURL, apiToken, requestId, customHeaders } = args;
|
|
69
|
+
const configuration = getRequestConfiguration({ baseURL, apiToken, requestId, customHeaders });
|
|
70
|
+
return Object.assign(Object.assign({}, configuration), { timeout: 30000, retry: {
|
|
71
|
+
retries: 3,
|
|
72
|
+
retryCondition: isNetworkError,
|
|
73
|
+
retryDelay: getRetryDelay,
|
|
74
|
+
} });
|
|
75
|
+
}
|
|
76
|
+
export function isSuccess(response) {
|
|
77
|
+
return response.status >= 200 && response.status < 300;
|
|
78
|
+
}
|
|
79
|
+
export async function request(args) {
|
|
80
|
+
const { httpMethod, baseUri, relativePath, apiToken, payload, requestId: initialRequestId, hasSyncCommands, customHeaders, customFetch, opaqueKeyMaps, } = args;
|
|
81
|
+
// Capture original stack for better error reporting
|
|
82
|
+
const originalStack = new Error();
|
|
83
|
+
try {
|
|
84
|
+
let requestId = initialRequestId;
|
|
85
|
+
// Sync api don't allow a request id in the CORS
|
|
86
|
+
if (httpMethod === 'POST' && !requestId && !baseUri.includes(API_BASE_URI)) {
|
|
87
|
+
requestId = uuidv4();
|
|
88
|
+
}
|
|
89
|
+
const config = getHttpClientConfig({ baseURL: baseUri, apiToken, requestId, customHeaders });
|
|
90
|
+
const url = `${baseUri}${relativePath}`;
|
|
91
|
+
const fetchOptions = {
|
|
92
|
+
method: httpMethod,
|
|
93
|
+
headers: config.headers,
|
|
94
|
+
timeout: config.timeout,
|
|
95
|
+
};
|
|
96
|
+
let finalUrl = url;
|
|
97
|
+
switch (httpMethod) {
|
|
98
|
+
case 'GET':
|
|
99
|
+
// For GET requests, add query parameters to URL
|
|
100
|
+
if (payload) {
|
|
101
|
+
// Convert payload from camelCase to snake_case
|
|
102
|
+
const convertedPayload = snakeCaseKeys(payload);
|
|
103
|
+
const queryString = paramsSerializer(convertedPayload);
|
|
104
|
+
if (queryString) {
|
|
105
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
106
|
+
finalUrl = `${url}${separator}${queryString}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case 'POST':
|
|
111
|
+
case 'PUT':
|
|
112
|
+
case 'PATCH':
|
|
113
|
+
case 'DELETE': {
|
|
114
|
+
// Convert payload from camelCase to snake_case
|
|
115
|
+
// Note: While DELETE with body is uncommon, the Todoist API uses it for some endpoints
|
|
116
|
+
if (payload) {
|
|
117
|
+
const convertedPayload = snakeCaseKeys(payload);
|
|
118
|
+
const body = hasSyncCommands
|
|
119
|
+
? JSON.stringify(convertedPayload)
|
|
120
|
+
: JSON.stringify(convertedPayload);
|
|
121
|
+
fetchOptions.body = body;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Make the request
|
|
127
|
+
const response = await fetchWithRetry({
|
|
128
|
+
url: finalUrl,
|
|
129
|
+
options: fetchOptions,
|
|
130
|
+
retryConfig: config.retry,
|
|
131
|
+
customFetch,
|
|
132
|
+
});
|
|
133
|
+
// Convert snake_case response to camelCase
|
|
134
|
+
const convertedData = camelCaseKeys(response.data, { opaqueKeyMaps });
|
|
135
|
+
return Object.assign(Object.assign({}, response), { data: convertedData });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw toTodoistRequestError(error, originalStack);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Use effectively-disabled keep-alive so short-lived CLI processes do not stay
|
|
2
|
+
// open waiting on idle sockets. Undici requires positive values, so we use 1ms.
|
|
3
|
+
const KEEP_ALIVE_OPTIONS = {
|
|
4
|
+
keepAliveTimeout: 1,
|
|
5
|
+
keepAliveMaxTimeout: 1,
|
|
6
|
+
};
|
|
7
|
+
let defaultTransport;
|
|
8
|
+
let defaultTransportPromise;
|
|
9
|
+
/**
|
|
10
|
+
* The default dispatcher and its paired `fetch`, as a single value so the two
|
|
11
|
+
* are always read consistently. Resolves to `undefined` outside Node (browser/
|
|
12
|
+
* edge), where callers use the global `fetch` with no dispatcher.
|
|
13
|
+
*
|
|
14
|
+
* Use this when supplying a `customFetch` that should still honour the proxy
|
|
15
|
+
* environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) and decode
|
|
16
|
+
* compressed responses the way the SDK does by default.
|
|
17
|
+
*
|
|
18
|
+
* Both halves must be used together. `dispatcher` decompresses the response
|
|
19
|
+
* body itself, so a `fetch` from a different undici build will try to decode
|
|
20
|
+
* it a second time and the request fails mid-stream with `terminated`. A
|
|
21
|
+
* `fetch` of `undefined` means the runtime's global `fetch` is the correct
|
|
22
|
+
* partner for this dispatcher.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const transport = await getDefaultTransport()
|
|
27
|
+
* const fetchImpl = transport?.fetch ?? fetch
|
|
28
|
+
* const response = await fetchImpl(url, { ...options, dispatcher: transport?.dispatcher })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function getDefaultTransport() {
|
|
32
|
+
if (!isNodeEnvironment()) {
|
|
33
|
+
return Promise.resolve(undefined);
|
|
34
|
+
}
|
|
35
|
+
if (!defaultTransportPromise) {
|
|
36
|
+
defaultTransportPromise = createDefaultTransport()
|
|
37
|
+
.then((transport) => {
|
|
38
|
+
defaultTransport = transport;
|
|
39
|
+
return transport;
|
|
40
|
+
})
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
defaultTransport = undefined;
|
|
43
|
+
defaultTransportPromise = undefined;
|
|
44
|
+
throw error;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return defaultTransportPromise;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The default dispatcher on its own.
|
|
51
|
+
*
|
|
52
|
+
* @deprecated Use {@link getDefaultTransport} instead, which returns this
|
|
53
|
+
* dispatcher together with the `fetch` it must be used with. Attaching this
|
|
54
|
+
* dispatcher to the runtime's global `fetch` decodes compressed responses
|
|
55
|
+
* twice on Node 26 and the request fails with `terminated`.
|
|
56
|
+
*/
|
|
57
|
+
export async function getDefaultDispatcher() {
|
|
58
|
+
var _a;
|
|
59
|
+
return (_a = (await getDefaultTransport())) === null || _a === void 0 ? void 0 : _a.dispatcher;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The `fetch` implementation that must be used with the default dispatcher.
|
|
63
|
+
* Returns undici's own `fetch` on the full-undici Node path, or `undefined`
|
|
64
|
+
* (meaning: use the global `fetch`) in the browser/edge/Bun paths.
|
|
65
|
+
*
|
|
66
|
+
* Node's global `fetch` is backed by whatever undici version ships inside that
|
|
67
|
+
* Node release (6.x on Node 22 … 8.x on Node 26). Our dispatcher — and its
|
|
68
|
+
* `decompress` interceptor — comes from the npm `undici` package, which is a
|
|
69
|
+
* different version. Handing an npm-undici dispatcher to a mismatched built-in
|
|
70
|
+
* client makes gzip responses fail mid-stream with `terminated`. Sourcing
|
|
71
|
+
* `fetch` from the same npm `undici` keeps the whole request path on one
|
|
72
|
+
* version and removes the split.
|
|
73
|
+
*
|
|
74
|
+
* Only meaningful after {@link getDefaultTransport} has resolved, which is the
|
|
75
|
+
* one place that populates it.
|
|
76
|
+
*/
|
|
77
|
+
export function getDefaultFetch() {
|
|
78
|
+
return defaultTransport === null || defaultTransport === void 0 ? void 0 : defaultTransport.fetch;
|
|
79
|
+
}
|
|
80
|
+
export async function resetDefaultDispatcherForTests() {
|
|
81
|
+
if (!defaultTransportPromise) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const transportPromise = defaultTransportPromise;
|
|
85
|
+
defaultTransport = undefined;
|
|
86
|
+
defaultTransportPromise = undefined;
|
|
87
|
+
await transportPromise.then((transport) => transport.dispatcher.close(), () => undefined);
|
|
88
|
+
}
|
|
89
|
+
async function createDefaultTransport() {
|
|
90
|
+
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
91
|
+
// undici into their bundle. `getDefaultTransport` already short-circuits
|
|
92
|
+
// outside Node, so this branch only runs when undici is safe to load.
|
|
93
|
+
const { EnvHttpProxyAgent, interceptors, fetch: undiciFetch } = await import('undici');
|
|
94
|
+
const agent = new EnvHttpProxyAgent(KEEP_ALIVE_OPTIONS);
|
|
95
|
+
// Some runtimes report `process.versions.node` (so `isNodeEnvironment()`
|
|
96
|
+
// passes) but ship only a partial undici: `interceptors.decompress` is
|
|
97
|
+
// absent and dispatchers have no `.compose`. Bun is the common case. There
|
|
98
|
+
// the proxy agent alone is enough — Bun's `fetch` decompresses
|
|
99
|
+
// gzip/deflate/br/zstd natively — so skip the interceptor instead of
|
|
100
|
+
// crashing on the missing API.
|
|
101
|
+
if (typeof interceptors.decompress !== 'function') {
|
|
102
|
+
// Bun: pair the agent with the global `fetch` (undefined), which
|
|
103
|
+
// decompresses natively.
|
|
104
|
+
return { dispatcher: agent, fetch: undefined };
|
|
105
|
+
}
|
|
106
|
+
// Compose the response-decompression interceptor so gzip/deflate/br/zstd
|
|
107
|
+
// bodies are decoded before consumers parse them. Required on Node 24+:
|
|
108
|
+
// attaching any custom dispatcher to the global `fetch` strips the
|
|
109
|
+
// `content-encoding` header but does not actually decompress the body,
|
|
110
|
+
// so callers receive raw gzipped bytes and `JSON.parse` fails.
|
|
111
|
+
// See https://github.com/todoist/todoist-cli/issues/318.
|
|
112
|
+
const decompress = suppressExperimentalWarningsSync(() => interceptors.decompress());
|
|
113
|
+
// Pair undici's own `fetch` with this dispatcher so the request client and
|
|
114
|
+
// the dispatcher stay on one undici version (see `getDefaultFetch`). The
|
|
115
|
+
// global `fetch` is backed by a different, Node-bundled undici; mixing the
|
|
116
|
+
// two makes the decompress interceptor terminate gzip responses on some
|
|
117
|
+
// Node versions.
|
|
118
|
+
return { dispatcher: agent.compose(decompress), fetch: undiciFetch };
|
|
119
|
+
}
|
|
120
|
+
// undici emits an `ExperimentalWarning` the first time `interceptors.decompress()`
|
|
121
|
+
// runs. The interceptor is stable for our gzipped-JSON-over-HTTPS use case;
|
|
122
|
+
// silence the warning during dispatcher init only so it does not leak to every
|
|
123
|
+
// consumer's stderr on the first request.
|
|
124
|
+
//
|
|
125
|
+
// `fn` must be synchronous so the override covers a single critical section
|
|
126
|
+
// (microseconds) — no unrelated `ExperimentalWarning` from elsewhere can
|
|
127
|
+
// interleave and be lost. We suppress every `ExperimentalWarning` rather than
|
|
128
|
+
// pattern-matching the message text: the message wording is an undici
|
|
129
|
+
// implementation detail (not a stable API), and the suppression window is
|
|
130
|
+
// narrow enough that a coarse type filter is safe.
|
|
131
|
+
//
|
|
132
|
+
// Exported for direct unit testing — the integration path through
|
|
133
|
+
// `getDefaultDispatcher()` cannot reliably exercise the helper because both
|
|
134
|
+
// the dispatcher singleton and undici's internal `warningEmitted` flag are
|
|
135
|
+
// once-per-process.
|
|
136
|
+
export function suppressExperimentalWarningsSync(fn) {
|
|
137
|
+
const originalEmit = process.emitWarning;
|
|
138
|
+
process.emitWarning = ((warning, typeOrOptions, ...rest) => {
|
|
139
|
+
const type = typeof typeOrOptions === 'string'
|
|
140
|
+
? typeOrOptions
|
|
141
|
+
: typeof typeOrOptions === 'object' && typeOrOptions !== null
|
|
142
|
+
? typeOrOptions.type
|
|
143
|
+
: undefined;
|
|
144
|
+
if (type === 'ExperimentalWarning')
|
|
145
|
+
return;
|
|
146
|
+
originalEmit.call(process, warning, typeOrOptions, ...rest);
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
return fn();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
process.emitWarning = originalEmit;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function isNodeEnvironment() {
|
|
156
|
+
var _a;
|
|
157
|
+
return typeof process !== 'undefined' && Boolean((_a = process.versions) === null || _a === void 0 ? void 0 : _a.node);
|
|
158
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ENDPOINT_SYNC } from '../consts/endpoints.js';
|
|
2
|
+
import { TodoistArgumentError, TodoistSyncError } from '../types/index.js';
|
|
3
|
+
import { DATE_FORMAT_TO_API, TIME_FORMAT_TO_API, DAY_OF_WEEK_TO_API, } from '../types/sync/index.js';
|
|
4
|
+
import { spreadIfDefined } from '../utils/request-helpers.js';
|
|
5
|
+
import { parseSyncResponse } from '../utils/validators.js';
|
|
6
|
+
import { request } from './http-client.js';
|
|
7
|
+
// An invalid Date would otherwise be sent as `null`, which the API reads as "no expiry".
|
|
8
|
+
function serializeStatusActiveUntil(value) {
|
|
9
|
+
if (!(value instanceof Date))
|
|
10
|
+
return value;
|
|
11
|
+
if (Number.isNaN(value.getTime())) {
|
|
12
|
+
throw new TodoistArgumentError('status.activeUntil is an invalid Date');
|
|
13
|
+
}
|
|
14
|
+
return value.toISOString();
|
|
15
|
+
}
|
|
16
|
+
function serializeUserUpdateArgs(args) {
|
|
17
|
+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, args), spreadIfDefined(args.dateFormat, (v) => ({
|
|
18
|
+
dateFormat: DATE_FORMAT_TO_API[v],
|
|
19
|
+
}))), spreadIfDefined(args.timeFormat, (v) => ({
|
|
20
|
+
timeFormat: TIME_FORMAT_TO_API[v],
|
|
21
|
+
}))), spreadIfDefined(args.startDay, (v) => ({
|
|
22
|
+
startDay: DAY_OF_WEEK_TO_API[v],
|
|
23
|
+
}))), spreadIfDefined(args.nextWeek, (v) => ({
|
|
24
|
+
nextWeek: DAY_OF_WEEK_TO_API[v],
|
|
25
|
+
}))), (args.status
|
|
26
|
+
? {
|
|
27
|
+
status: Object.assign(Object.assign({}, args.status), spreadIfDefined(args.status.activeUntil, (v) => ({
|
|
28
|
+
activeUntil: serializeStatusActiveUntil(v),
|
|
29
|
+
}))),
|
|
30
|
+
}
|
|
31
|
+
: {}));
|
|
32
|
+
}
|
|
33
|
+
function serializeTaskUpdateDateCompleteArgs(args) {
|
|
34
|
+
return Object.assign(Object.assign(Object.assign({}, args), { isForward: args.isForward ? 1 : 0 }), spreadIfDefined(args.resetSubtasks, (v) => ({
|
|
35
|
+
resetSubtasks: v ? 1 : 0,
|
|
36
|
+
})));
|
|
37
|
+
}
|
|
38
|
+
function serializeUpdateGoalsArgs(args) {
|
|
39
|
+
return Object.assign(Object.assign(Object.assign(Object.assign({}, args), spreadIfDefined(args.vacationMode, (v) => ({ vacationMode: v ? 1 : 0 }))), spreadIfDefined(args.karmaDisabled, (v) => ({
|
|
40
|
+
karmaDisabled: v ? 1 : 0,
|
|
41
|
+
}))), spreadIfDefined(args.ignoreDays, (v) => ({
|
|
42
|
+
ignoreDays: v.map((day) => {
|
|
43
|
+
const apiValue = DAY_OF_WEEK_TO_API[day];
|
|
44
|
+
if (apiValue === undefined) {
|
|
45
|
+
throw new TodoistArgumentError(`Invalid ignoreDays entry "${String(day)}". Expected a DayOfWeek (e.g. "Monday").`);
|
|
46
|
+
}
|
|
47
|
+
return apiValue;
|
|
48
|
+
}),
|
|
49
|
+
})));
|
|
50
|
+
}
|
|
51
|
+
function preprocessSyncCommands(commands) {
|
|
52
|
+
return commands.map((cmd) => {
|
|
53
|
+
if (cmd.type === 'user_update')
|
|
54
|
+
return Object.assign(Object.assign({}, cmd), { args: serializeUserUpdateArgs(cmd.args) });
|
|
55
|
+
if (cmd.type === 'item_update_date_complete')
|
|
56
|
+
return Object.assign(Object.assign({}, cmd), { args: serializeTaskUpdateDateCompleteArgs(cmd.args) });
|
|
57
|
+
if (cmd.type === 'update_goals')
|
|
58
|
+
return Object.assign(Object.assign({}, cmd), { args: serializeUpdateGoalsArgs(cmd.args) });
|
|
59
|
+
return cmd;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Response fields keyed by the command `uuid` / `tempId` the caller sent. Their
|
|
64
|
+
* keys must reach the caller unchanged so each entry can be matched to its
|
|
65
|
+
* command; camel-casing would rewrite them (`a3bb189e-8bf9-…` → `a3Bb189E8Bf9…`).
|
|
66
|
+
*/
|
|
67
|
+
const COMMAND_KEYED_FIELDS = new Set(['sync_status', 'temp_id_mapping']);
|
|
68
|
+
/**
|
|
69
|
+
* Makes a request to the Sync API and handles error checking.
|
|
70
|
+
*
|
|
71
|
+
* @param ctx - HTTP identity (auth token, sync API base URI, optional custom fetch and headers).
|
|
72
|
+
* @param syncRequest - The sync request payload.
|
|
73
|
+
* @param options - Optional request identifier and commands flag.
|
|
74
|
+
* @returns The sync response data, with `syncStatus` keyed by command `uuid` and
|
|
75
|
+
* `tempIdMapping` keyed by command `tempId`.
|
|
76
|
+
* @throws TodoistSyncError if any command failed. Commands are applied
|
|
77
|
+
* independently, so its `syncStatus` and `tempIdMapping` report the ones that
|
|
78
|
+
* were applied regardless.
|
|
79
|
+
*/
|
|
80
|
+
export async function performSyncRequest(ctx, syncRequest, options = {}) {
|
|
81
|
+
var _a, _b;
|
|
82
|
+
const { requestId, hasSyncCommands = false } = options;
|
|
83
|
+
const processedRequest = ((_a = syncRequest.commands) === null || _a === void 0 ? void 0 : _a.length)
|
|
84
|
+
? Object.assign(Object.assign({}, syncRequest), { commands: preprocessSyncCommands(syncRequest.commands) }) : syncRequest;
|
|
85
|
+
const response = await request({
|
|
86
|
+
httpMethod: 'POST',
|
|
87
|
+
baseUri: ctx.syncApiBase,
|
|
88
|
+
relativePath: ENDPOINT_SYNC,
|
|
89
|
+
apiToken: ctx.authToken,
|
|
90
|
+
customFetch: ctx.customFetch,
|
|
91
|
+
customHeaders: ctx.customHeaders,
|
|
92
|
+
payload: processedRequest,
|
|
93
|
+
requestId: requestId,
|
|
94
|
+
hasSyncCommands: hasSyncCommands,
|
|
95
|
+
opaqueKeyMaps: COMMAND_KEYED_FIELDS,
|
|
96
|
+
});
|
|
97
|
+
const syncStatus = response.data.syncStatus;
|
|
98
|
+
const firstFailure = Object.values(syncStatus !== null && syncStatus !== void 0 ? syncStatus : {}).find((status) => status !== 'ok');
|
|
99
|
+
if (firstFailure) {
|
|
100
|
+
throw new TodoistSyncError(firstFailure, {
|
|
101
|
+
syncStatus: syncStatus !== null && syncStatus !== void 0 ? syncStatus : {},
|
|
102
|
+
tempIdMapping: (_b = response.data.tempIdMapping) !== null && _b !== void 0 ? _b : {},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return parseSyncResponse(response.data);
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Flexible object containing event-specific data.
|
|
24
|
+
* Uses z.record to accept any properties for forward compatibility.
|
|
25
|
+
*/
|
|
26
|
+
export const ActivityEventExtraDataSchema = z.record(z.string(), z.any()).nullable();
|
|
27
|
+
/**
|
|
28
|
+
* Activity log event schema. Accepts unknown fields for forward compatibility.
|
|
29
|
+
*/
|
|
30
|
+
export const ActivityEventSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
objectType: z.string(),
|
|
33
|
+
objectId: z.string(),
|
|
34
|
+
eventType: z.string(),
|
|
35
|
+
eventDate: z.coerce.date(),
|
|
36
|
+
id: z
|
|
37
|
+
.union([z.string(), z.number()])
|
|
38
|
+
.transform((val) => { var _a; return (_a = val === null || val === void 0 ? void 0 : val.toString()) !== null && _a !== void 0 ? _a : null; })
|
|
39
|
+
.nullable(),
|
|
40
|
+
parentProjectId: z.string().nullable(),
|
|
41
|
+
parentItemId: z.string().nullable(),
|
|
42
|
+
initiatorId: z.string().nullable(),
|
|
43
|
+
extraData: ActivityEventExtraDataSchema,
|
|
44
|
+
})
|
|
45
|
+
.catchall(z.any());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|