@todoist/sdk 0.0.1 → 19.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 +368 -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 +255 -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 +99 -0
- package/dist/types/types/apps/types.d.ts +450 -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 +1922 -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,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a native Fetch `Headers` object into a plain `Record<string, string>`.
|
|
3
|
+
* Used when wrapping a native `fetch` response into the SDK's `FileResponse`
|
|
4
|
+
* shape (which expects headers as a plain record for symmetry with
|
|
5
|
+
* `CustomFetchResponse`).
|
|
6
|
+
*/
|
|
7
|
+
export function headersToRecord(headers) {
|
|
8
|
+
const result = {};
|
|
9
|
+
headers.forEach((value, key) => {
|
|
10
|
+
result[key] = value;
|
|
11
|
+
});
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Merges header records left to right. HTTP header names are case-insensitive,
|
|
16
|
+
* so a later record replaces any earlier header with the same name whatever
|
|
17
|
+
* its case. Without this, a caller's `authorization` would sit alongside the
|
|
18
|
+
* SDK's `Authorization` and `fetch` would join the two into one value.
|
|
19
|
+
*/
|
|
20
|
+
export function mergeHeaders(...sources) {
|
|
21
|
+
const merged = {};
|
|
22
|
+
for (const source of sources) {
|
|
23
|
+
if (!source)
|
|
24
|
+
continue;
|
|
25
|
+
for (const [name, value] of Object.entries(source)) {
|
|
26
|
+
const lowerName = name.toLowerCase();
|
|
27
|
+
for (const existingName of Object.keys(merged)) {
|
|
28
|
+
if (existingName.toLowerCase() === lowerName)
|
|
29
|
+
delete merged[existingName];
|
|
30
|
+
}
|
|
31
|
+
merged[name] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return merged;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Turns the `headers` option into the record to send with one request, calling
|
|
38
|
+
* it when it is a function so per-request values are fresh.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveCustomHeaders(headers) {
|
|
41
|
+
return typeof headers === 'function' ? headers() : headers;
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './colors.js';
|
|
2
|
+
export * from './sanitization.js';
|
|
3
|
+
export { getTaskUrl, getProjectUrl, getSectionUrl, getLabelUrl, getFilterUrl, getWorkspaceFilterUrl, getWorkspaceUrl, getTaskCommentsUrl, getProjectCommentsUrl, getTaskCommentUrl, getProjectCommentUrl, parseTodoistUrl, TODOIST_LINK_TYPES, } from './url-helpers.js';
|
|
4
|
+
export { createCommand } from './sync-helpers.js';
|
|
5
|
+
export * from './project-helpers.js';
|
|
6
|
+
export * from './person-names.js';
|
|
7
|
+
export { isDateDrivenQuery } from './filter-queries.js';
|
|
8
|
+
export { FILTER_QUERY_LANGUAGES } from './filter-query-definitions.js';
|
|
9
|
+
export * from './task-sorting.js';
|
|
10
|
+
export * from './view-options.js';
|
|
11
|
+
export { parseWebhookPayload, verifyWebhookSignature } from './webhook-parser.js';
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
|
|
2
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
3
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
4
|
+
var m = o[Symbol.asyncIterator], i;
|
|
5
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
6
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
7
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
8
|
+
};
|
|
9
|
+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
|
|
10
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
11
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
12
|
+
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
|
|
13
|
+
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
|
|
14
|
+
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
|
|
15
|
+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
|
16
|
+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
|
17
|
+
function fulfill(value) { resume("next", value); }
|
|
18
|
+
function reject(value) { resume("throw", value); }
|
|
19
|
+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
|
20
|
+
};
|
|
21
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
22
|
+
import { fetchWithRetry } from '../transport/fetch-with-retry.js';
|
|
23
|
+
import { toTodoistRequestError } from '../transport/http-client.js';
|
|
24
|
+
import { camelCaseKeys } from './case-conversion.js';
|
|
25
|
+
import { mergeHeaders, resolveCustomHeaders } from './headers.js';
|
|
26
|
+
/**
|
|
27
|
+
* Helper function to determine content-type from filename extension.
|
|
28
|
+
* @param fileName - The filename to analyze
|
|
29
|
+
* @returns The appropriate MIME type
|
|
30
|
+
*/
|
|
31
|
+
function getContentTypeFromFileName(fileName) {
|
|
32
|
+
const extension = fileName.toLowerCase().split('.').pop();
|
|
33
|
+
switch (extension) {
|
|
34
|
+
case 'png':
|
|
35
|
+
return 'image/png';
|
|
36
|
+
case 'jpg':
|
|
37
|
+
case 'jpeg':
|
|
38
|
+
return 'image/jpeg';
|
|
39
|
+
case 'gif':
|
|
40
|
+
return 'image/gif';
|
|
41
|
+
case 'webp':
|
|
42
|
+
return 'image/webp';
|
|
43
|
+
case 'svg':
|
|
44
|
+
return 'image/svg+xml';
|
|
45
|
+
default:
|
|
46
|
+
return 'application/octet-stream';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Escapes a value for use inside a quoted `Content-Disposition` parameter,
|
|
51
|
+
* where a raw quote would end the value early and a raw CR or LF would end the
|
|
52
|
+
* header. Note that a lone CR needs escaping too, not just CRLF pairs.
|
|
53
|
+
*
|
|
54
|
+
* This is the same escaping the platform's own `FormData` encoder applies, so
|
|
55
|
+
* servers see exactly what they would from any browser or from undici.
|
|
56
|
+
*/
|
|
57
|
+
function escapeDispositionValue(value) {
|
|
58
|
+
return value.replace(/"/g, '%22').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Lays out a `multipart/form-data` payload around a single file part, as the
|
|
62
|
+
* text that goes before and after the file content. Keeping the file content
|
|
63
|
+
* out of it lets the same layout serve both a `Blob` body and a streamed one.
|
|
64
|
+
*/
|
|
65
|
+
function layOutMultipart(args) {
|
|
66
|
+
const { fileName, fileContentType, additionalFields } = args;
|
|
67
|
+
const boundary = `----todoist-sdk-${uuidv4()}`;
|
|
68
|
+
const head = `--${boundary}\r\n` +
|
|
69
|
+
`Content-Disposition: form-data; name="file"; filename="${escapeDispositionValue(fileName)}"\r\n` +
|
|
70
|
+
`Content-Type: ${fileContentType}\r\n\r\n`;
|
|
71
|
+
let tail = '\r\n';
|
|
72
|
+
for (const [key, value] of Object.entries(additionalFields)) {
|
|
73
|
+
if (value !== undefined && value !== null) {
|
|
74
|
+
tail +=
|
|
75
|
+
`--${boundary}\r\n` +
|
|
76
|
+
`Content-Disposition: form-data; name="${escapeDispositionValue(key)}"\r\n\r\n` +
|
|
77
|
+
`${value.toString()}\r\n`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
tail += `--${boundary}--\r\n`;
|
|
81
|
+
return { boundary, head, tail };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Wraps a Node readable stream as a `multipart/form-data` request body.
|
|
85
|
+
*
|
|
86
|
+
* The result is a web `ReadableStream`, which both the runtime's `fetch` and
|
|
87
|
+
* undici's own accept — unlike a Node stream or a `FormData`, either of which
|
|
88
|
+
* only works with one particular client. The file is never buffered, so the
|
|
89
|
+
* request is sent with `Transfer-Encoding: chunked`.
|
|
90
|
+
*/
|
|
91
|
+
function streamMultipartBody(file, parts) {
|
|
92
|
+
const encoder = new TextEncoder();
|
|
93
|
+
function chunks() {
|
|
94
|
+
return __asyncGenerator(this, arguments, function* chunks_1() {
|
|
95
|
+
var _a, e_1, _b, _c;
|
|
96
|
+
yield yield __await(encoder.encode(parts.head));
|
|
97
|
+
try {
|
|
98
|
+
for (var _d = true, file_1 = __asyncValues(file), file_1_1; file_1_1 = yield __await(file_1.next()), _a = file_1_1.done, !_a; _d = true) {
|
|
99
|
+
_c = file_1_1.value;
|
|
100
|
+
_d = false;
|
|
101
|
+
const chunk = _c;
|
|
102
|
+
yield yield __await(typeof chunk === 'string' ? encoder.encode(chunk) : new Uint8Array(chunk));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
106
|
+
finally {
|
|
107
|
+
try {
|
|
108
|
+
if (!_d && !_a && (_b = file_1.return)) yield __await(_b.call(file_1));
|
|
109
|
+
}
|
|
110
|
+
finally { if (e_1) throw e_1.error; }
|
|
111
|
+
}
|
|
112
|
+
yield yield __await(encoder.encode(parts.tail));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const iterator = chunks();
|
|
116
|
+
// Driven by hand rather than with `ReadableStream.from`, which is absent
|
|
117
|
+
// from this project's DOM lib types and is still missing in some browsers,
|
|
118
|
+
// or `Readable.toWeb`, which would drag a Node-only import into a module
|
|
119
|
+
// that browsers also load.
|
|
120
|
+
return new ReadableStream({
|
|
121
|
+
async pull(controller) {
|
|
122
|
+
const { value, done } = await iterator.next();
|
|
123
|
+
if (done) {
|
|
124
|
+
controller.close();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
controller.enqueue(value);
|
|
128
|
+
},
|
|
129
|
+
async cancel(reason) {
|
|
130
|
+
var _a;
|
|
131
|
+
await ((_a = iterator.return) === null || _a === void 0 ? void 0 : _a.call(iterator, reason));
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Builds the request body for a multipart upload.
|
|
137
|
+
*
|
|
138
|
+
* Deliberately avoids `FormData`. A `FormData` body is only encoded by the
|
|
139
|
+
* `fetch` that owns that `FormData` class — undici brands its own — so a body
|
|
140
|
+
* built from the global `FormData` and sent through undici's `fetch`, or the
|
|
141
|
+
* reverse, is stringified to the literal `"[object FormData]"` and the upload
|
|
142
|
+
* silently carries no file. The `form-data` package fares worse: neither
|
|
143
|
+
* `fetch` accepts it as a body at all. `Blob` and `ReadableStream` carry no
|
|
144
|
+
* such brand and work with whichever `fetch` dispatches the request,
|
|
145
|
+
* including a caller-supplied `customFetch`.
|
|
146
|
+
*/
|
|
147
|
+
function buildMultipartBody(args) {
|
|
148
|
+
const { file, fileName, fileContentType, additionalFields } = args;
|
|
149
|
+
const parts = layOutMultipart({ fileName, fileContentType, additionalFields });
|
|
150
|
+
const contentType = `multipart/form-data; boundary=${parts.boundary}`;
|
|
151
|
+
if (file instanceof Blob) {
|
|
152
|
+
// A Blob keeps its length, so the request gets a Content-Length and
|
|
153
|
+
// stays replayable if it has to be retried. File-backed Blobs (from
|
|
154
|
+
// `fs.openAsBlob`) stay lazy here, so nothing is read into memory.
|
|
155
|
+
return { body: new Blob([parts.head, file, parts.tail]), contentType, isStreamed: false };
|
|
156
|
+
}
|
|
157
|
+
return { body: streamMultipartBody(file, parts), contentType, isStreamed: true };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Uploads a file using multipart/form-data.
|
|
161
|
+
*
|
|
162
|
+
* This is a shared utility for uploading files to Todoist endpoints that require
|
|
163
|
+
* multipart/form-data content type (e.g., file uploads, workspace logo uploads).
|
|
164
|
+
*
|
|
165
|
+
* Supports both browser (Blob/File) and Node.js (Buffer/ReadableStream/path) environments.
|
|
166
|
+
*
|
|
167
|
+
* @param baseUrl - The base API URL (e.g., https://api.todoist.com/api/v1/)
|
|
168
|
+
* @param authToken - The authentication token
|
|
169
|
+
* @param endpoint - The relative endpoint path (e.g., 'uploads', 'workspaces/logo')
|
|
170
|
+
* @param file - The file content (Blob/File for browser, or Buffer/ReadableStream/path for Node)
|
|
171
|
+
* @param fileName - Optional file name (required for Buffer/Stream, optional for paths and File objects)
|
|
172
|
+
* @param additionalFields - Additional form fields to include (e.g., project_id, workspace_id)
|
|
173
|
+
* @param requestId - Optional request ID for idempotency
|
|
174
|
+
* @returns The response data from the server
|
|
175
|
+
* @throws TodoistRequestError if the upload fails, carrying the HTTP status and
|
|
176
|
+
* the server's error body like any other API request.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* // Upload from a file path
|
|
181
|
+
* const result = await uploadMultipartFile(
|
|
182
|
+
* 'https://api.todoist.com/api/v1/',
|
|
183
|
+
* 'my-token',
|
|
184
|
+
* 'uploads',
|
|
185
|
+
* '/path/to/file.pdf',
|
|
186
|
+
* undefined,
|
|
187
|
+
* { project_id: '12345' }
|
|
188
|
+
* )
|
|
189
|
+
*
|
|
190
|
+
* // Upload from a Buffer
|
|
191
|
+
* const buffer = Buffer.from('file content')
|
|
192
|
+
* const result = await uploadMultipartFile(
|
|
193
|
+
* 'https://api.todoist.com/api/v1/',
|
|
194
|
+
* 'my-token',
|
|
195
|
+
* 'uploads',
|
|
196
|
+
* buffer,
|
|
197
|
+
* 'document.pdf',
|
|
198
|
+
* { project_id: '12345' }
|
|
199
|
+
* )
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export async function uploadMultipartFile(args) {
|
|
203
|
+
const { baseUrl, authToken, endpoint, file, fileName, additionalFields, requestId, customHeaders, customFetch, } = args;
|
|
204
|
+
// Capture original stack for better error reporting
|
|
205
|
+
const originalStack = new Error();
|
|
206
|
+
// Build the full URL
|
|
207
|
+
const url = `${baseUrl}${endpoint}`;
|
|
208
|
+
let body;
|
|
209
|
+
// Resolve the file to something every `fetch` accepts as a body part: a
|
|
210
|
+
// Blob where the size is known up front, or a Node stream that gets piped
|
|
211
|
+
// through untouched.
|
|
212
|
+
let filePart;
|
|
213
|
+
let resolvedFileName;
|
|
214
|
+
if (file instanceof Blob) {
|
|
215
|
+
filePart = file;
|
|
216
|
+
resolvedFileName = fileName || (file instanceof File ? file.name : undefined) || 'upload';
|
|
217
|
+
}
|
|
218
|
+
else if (typeof file === 'string') {
|
|
219
|
+
// File path: Node-only, so import lazily to keep browser bundles clean.
|
|
220
|
+
const [fsModule, pathModule] = await Promise.all([import('fs'), import('path')]);
|
|
221
|
+
resolvedFileName = fileName || pathModule.basename(file);
|
|
222
|
+
// `openAsBlob` reads the file lazily, so a large upload is not pulled
|
|
223
|
+
// into memory, and the resulting body still has a known length.
|
|
224
|
+
filePart = await fsModule.openAsBlob(file);
|
|
225
|
+
}
|
|
226
|
+
else if (Buffer.isBuffer(file)) {
|
|
227
|
+
if (!fileName) {
|
|
228
|
+
throw new Error('fileName is required when uploading from a Buffer');
|
|
229
|
+
}
|
|
230
|
+
resolvedFileName = fileName;
|
|
231
|
+
filePart = new Blob([
|
|
232
|
+
new Uint8Array(file.buffer, file.byteOffset, file.byteLength),
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
if (!fileName) {
|
|
237
|
+
throw new Error('fileName is required when uploading from a stream');
|
|
238
|
+
}
|
|
239
|
+
resolvedFileName = fileName;
|
|
240
|
+
filePart = file;
|
|
241
|
+
}
|
|
242
|
+
const multipart = buildMultipartBody({
|
|
243
|
+
file: filePart,
|
|
244
|
+
fileName: resolvedFileName,
|
|
245
|
+
fileContentType: (filePart instanceof Blob ? filePart.type : undefined) ||
|
|
246
|
+
getContentTypeFromFileName(resolvedFileName),
|
|
247
|
+
additionalFields,
|
|
248
|
+
});
|
|
249
|
+
// Custom headers come first so they can never replace the SDK's own.
|
|
250
|
+
const headers = mergeHeaders(resolveCustomHeaders(customHeaders), Object.assign(Object.assign({ Authorization: `Bearer ${authToken}` }, (requestId ? { 'X-Request-Id': requestId } : {})), {
|
|
251
|
+
// The boundary is ours, so Content-Type has to be set explicitly —
|
|
252
|
+
// `fetch` only fills it in for a `FormData` body.
|
|
253
|
+
'Content-Type': multipart.contentType }));
|
|
254
|
+
body = multipart.body;
|
|
255
|
+
// Make the request using fetch
|
|
256
|
+
let response;
|
|
257
|
+
try {
|
|
258
|
+
response = await fetchWithRetry(Object.assign(Object.assign({ url, options: Object.assign({ method: 'POST', body,
|
|
259
|
+
headers, timeout: 30000 }, (multipart.isStreamed ? { duplex: 'half' } : {})) }, (multipart.isStreamed ? { retryConfig: { retries: 0 } } : {})), { customFetch }));
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
// `fetchWithRetry` throws a plain `Error` for non-2xx responses; convert
|
|
263
|
+
// it so callers get the same error type as every other API request.
|
|
264
|
+
throw toTodoistRequestError(error, originalStack);
|
|
265
|
+
}
|
|
266
|
+
return camelCaseKeys(response.data);
|
|
267
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getFirstName as sharedGetFirstName, getLastName as sharedGetLastName, getShortName as sharedGetShortName, } from '@doist/sdk-kmp/shared-universe-all';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the first word of a person's name, or an empty string if the name is blank.
|
|
4
|
+
*
|
|
5
|
+
* Status text people add to their display name isn't part of the name. Anything after a `|`, `@`
|
|
6
|
+
* or dash with spaces around it is left out, as is a trailing `OOO` or note in parentheses, so
|
|
7
|
+
* `Ada Lovelace (OOO Friday)` and `Ada Lovelace @ Paternity Leave` both read as `Ada Lovelace`.
|
|
8
|
+
*
|
|
9
|
+
* @param fullName - The person's full name, such as a user's `name` or `fullName`
|
|
10
|
+
* @returns The first name, or an empty string for a blank name
|
|
11
|
+
*/
|
|
12
|
+
export function getFirstName(fullName) {
|
|
13
|
+
return sharedGetFirstName(fullName);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Returns the last word of a person's name, or an empty string if the name is blank. A one-word
|
|
17
|
+
* name is its own last name.
|
|
18
|
+
*
|
|
19
|
+
* Status text is left out as it is for {@link getFirstName}, so `Ada Lovelace (OOO Friday)` gives
|
|
20
|
+
* `Lovelace`. A trailing emoji still counts as a word here, so `Ana Silva 🌴` gives `🌴`, while
|
|
21
|
+
* {@link getShortName} skips it when picking the initial.
|
|
22
|
+
*
|
|
23
|
+
* @param fullName - The person's full name, such as a user's `name` or `fullName`
|
|
24
|
+
* @returns The last name, or an empty string for a blank name
|
|
25
|
+
*/
|
|
26
|
+
export function getLastName(fullName) {
|
|
27
|
+
return sharedGetLastName(fullName);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns a person's first name and the initial of their last name, such as `Richard A.` for
|
|
31
|
+
* `Richard Dean Anderson`.
|
|
32
|
+
*
|
|
33
|
+
* - Status text is left out as it is for {@link getFirstName}, so `Ada Lovelace (OOO Friday)`
|
|
34
|
+
* gives `Ada L.`, not `Ada F.`.
|
|
35
|
+
* - The initial comes from the last word rather than the second, because in a name of three or
|
|
36
|
+
* more words the last one is usually the family name.
|
|
37
|
+
* - The initial is a whole character as the reader sees it, so an accent or an Indic conjunct
|
|
38
|
+
* stays with its letter: `Shreya श्रीवास्तव` gives `Shreya श्री.`.
|
|
39
|
+
* - A word that doesn't start with a letter, such as a trailing emoji, is skipped, so
|
|
40
|
+
* `Ana Silva 🌴` gives `Ana S.`. When no word after the first starts with a letter, as in
|
|
41
|
+
* `Robot 🤖` or a one-word name, the first name is returned alone.
|
|
42
|
+
*
|
|
43
|
+
* @param fullName - The person's full name, such as a user's `name` or `fullName`
|
|
44
|
+
* @returns The first name and last initial, the first name alone when there's no initial, or an
|
|
45
|
+
* empty string for a blank name
|
|
46
|
+
*/
|
|
47
|
+
export function getShortName(fullName) {
|
|
48
|
+
return sharedGetShortName(fullName);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns a person's first name and the initial of their last name.
|
|
52
|
+
*
|
|
53
|
+
* @deprecated Use {@link getShortName} instead, which this calls. It used to take the initial
|
|
54
|
+
* from the second word, which no longer matches the Todoist apps.
|
|
55
|
+
*
|
|
56
|
+
* @param fullName - The person's full name, such as a user's `name` or `fullName`
|
|
57
|
+
* @returns The same as {@link getShortName}
|
|
58
|
+
*/
|
|
59
|
+
export function formatUserShortName(fullName) {
|
|
60
|
+
return getShortName(fullName);
|
|
61
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import camelcase from 'camelcase';
|
|
2
|
+
import emojiRegex from 'emoji-regex';
|
|
3
|
+
function isEmojiKey(key) {
|
|
4
|
+
const regex = emojiRegex();
|
|
5
|
+
return regex.test(key);
|
|
6
|
+
}
|
|
7
|
+
export function customCamelCase(input) {
|
|
8
|
+
// If the value is a solitary emoji string, return the key as-is
|
|
9
|
+
if (isEmojiKey(input))
|
|
10
|
+
return input;
|
|
11
|
+
return camelcase(input);
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard to check if a project is a workspace project.
|
|
3
|
+
* @param project The project to check
|
|
4
|
+
* @returns True if the project is a workspace project
|
|
5
|
+
*/
|
|
6
|
+
export function isWorkspaceProject(project) {
|
|
7
|
+
return 'workspaceId' in project;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Type guard to check if a project is a personal project.
|
|
11
|
+
* @param project The project to check
|
|
12
|
+
* @returns True if the project is a personal project
|
|
13
|
+
*/
|
|
14
|
+
export function isPersonalProject(project) {
|
|
15
|
+
return !isWorkspaceProject(project);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether a project counts towards the plan's limit on active personal projects:
|
|
19
|
+
* an active personal project other than the Inbox. Shared projects and sub-projects
|
|
20
|
+
* count; workspace projects do not.
|
|
21
|
+
* @param project The project to check
|
|
22
|
+
* @returns True if the project counts towards the limit
|
|
23
|
+
*/
|
|
24
|
+
export function countsTowardsPersonalProjectLimit(project) {
|
|
25
|
+
return (isPersonalProject(project) &&
|
|
26
|
+
!project.inboxProject &&
|
|
27
|
+
!project.isArchived &&
|
|
28
|
+
!project.isDeleted);
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { TodoistArgumentError } from '../types/errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encodes a caller-supplied value (an ID, a name) for use as one URL path segment.
|
|
4
|
+
*
|
|
5
|
+
* Without this, a value such as `../projects/123` would be joined into the path
|
|
6
|
+
* as-is and the URL would resolve to a different endpoint. `encodeURIComponent`
|
|
7
|
+
* escapes `/`, `?`, `#` and `%`, but leaves `.` alone, so the dot segments `.`
|
|
8
|
+
* and `..` (and the empty segment) are rejected outright. `,` is kept literal,
|
|
9
|
+
* as it is a valid path character and joins lists of IDs.
|
|
10
|
+
*
|
|
11
|
+
* @param segment The value to encode.
|
|
12
|
+
* @returns The encoded segment.
|
|
13
|
+
* @throws {TodoistArgumentError} When the value is empty, `.` or `..`.
|
|
14
|
+
*/
|
|
15
|
+
export function encodePathSegment(segment) {
|
|
16
|
+
if (segment === '' || segment === '.' || segment === '..') {
|
|
17
|
+
throw new TodoistArgumentError(`Invalid path segment: "${segment}"`);
|
|
18
|
+
}
|
|
19
|
+
return encodeURIComponent(segment).replace(/%2C/gi, ',');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Joins an endpoint with path segments using `/` separator.
|
|
23
|
+
*
|
|
24
|
+
* The endpoint is used as-is (it may contain `/`); every following segment is
|
|
25
|
+
* encoded with {@link encodePathSegment}, so an ID can never address another
|
|
26
|
+
* resource.
|
|
27
|
+
*
|
|
28
|
+
* @param endpoint The base endpoint, e.g. `tasks`.
|
|
29
|
+
* @param segments Path segments to append, e.g. an ID and an action.
|
|
30
|
+
* @returns A joined path.
|
|
31
|
+
*/
|
|
32
|
+
export function generatePath(endpoint, ...segments) {
|
|
33
|
+
return [endpoint, ...segments.map(encodePathSegment)].join('/');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns `fn(value)` spread into an object if `value` is defined, otherwise an empty object.
|
|
37
|
+
*
|
|
38
|
+
* Used to conditionally include a property on a payload object without
|
|
39
|
+
* resorting to mutation or ternary-built object literals.
|
|
40
|
+
*/
|
|
41
|
+
export function spreadIfDefined(value, fn) {
|
|
42
|
+
return value !== undefined ? fn(value) : {};
|
|
43
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const BOLD_FORMAT = /(^|[\s!?,;>:]+)(?:\*\*|__|!!)(.+?)(\*\*|__|!!)(?=$|[\s!?,;><:]+)/gi;
|
|
2
|
+
const ITALIC_FORMAT = /(^|[\s!?,;>:]+)(?:\*|_|!)(.+?)(\*|_|!)(?=$|[\s!?,;><:]+)/gi;
|
|
3
|
+
const BOLD_ITALIC_FORMAT = /(^|[\s!?,;>:]+)(?:\*\*\*|___|!!!)(.+?)(\*\*\*|___|!!!)(?=$|[\s!?,;><:]+)/gi;
|
|
4
|
+
const CODE_BLOCK_FORMAT = /```([\s\S]*?)```/gi;
|
|
5
|
+
const CODE_INLINE_FORMAT = /`([^`]+)`/gi;
|
|
6
|
+
const TODOIST_LINK = /((?:(?:onenote:)?[\w-]+):\/\/[^\s]+)\s+[[(]([^)]+)[\])]/gi;
|
|
7
|
+
const MARKDOWN_LINK = /\[(.+?)\]\((.+?)\)/gi;
|
|
8
|
+
const GMAIL_LINK = /\[\[gmail=(.+?),\s*(.+?)\]\]/gi;
|
|
9
|
+
const OUTLOOK_LINK = /\[\[outlook=(.+?),\s*(.+?)\]\]/gi;
|
|
10
|
+
const THUNDERBIRD_LINK = /\[\[thunderbird\n(.+)\n(.+)\n\s*\]\]/gi;
|
|
11
|
+
const FAKE_SECTION_PREFIX = '* ';
|
|
12
|
+
const FAKE_SECTION_SUFFIX = ':';
|
|
13
|
+
function removeStyleFormatting(input) {
|
|
14
|
+
if (!input.includes('!') && !input.includes('*') && !input.includes('_')) {
|
|
15
|
+
return input;
|
|
16
|
+
}
|
|
17
|
+
function removeMarkdown(_match, prefix, text) {
|
|
18
|
+
return `${prefix}${text}`;
|
|
19
|
+
}
|
|
20
|
+
input = input.replace(BOLD_ITALIC_FORMAT, removeMarkdown);
|
|
21
|
+
input = input.replace(BOLD_FORMAT, removeMarkdown);
|
|
22
|
+
input = input.replace(ITALIC_FORMAT, removeMarkdown);
|
|
23
|
+
return input;
|
|
24
|
+
}
|
|
25
|
+
function removeCodeFormatting(input) {
|
|
26
|
+
function removeMarkdown(_match, text) {
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
input = input.replace(CODE_BLOCK_FORMAT, removeMarkdown);
|
|
30
|
+
input = input.replace(CODE_INLINE_FORMAT, removeMarkdown);
|
|
31
|
+
return input;
|
|
32
|
+
}
|
|
33
|
+
function removeFakeSectionFormatting(input) {
|
|
34
|
+
if (input.startsWith(FAKE_SECTION_PREFIX)) {
|
|
35
|
+
input = input.slice(FAKE_SECTION_PREFIX.length);
|
|
36
|
+
}
|
|
37
|
+
if (input.endsWith(FAKE_SECTION_SUFFIX)) {
|
|
38
|
+
input = input.slice(0, input.length - FAKE_SECTION_SUFFIX.length);
|
|
39
|
+
}
|
|
40
|
+
return input;
|
|
41
|
+
}
|
|
42
|
+
function removeMarkdownLinks(input) {
|
|
43
|
+
if (!input.includes('[') || !input.includes(']')) {
|
|
44
|
+
return input;
|
|
45
|
+
}
|
|
46
|
+
function removeMarkdown(_match, text) {
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
return input.replace(MARKDOWN_LINK, removeMarkdown);
|
|
50
|
+
}
|
|
51
|
+
function removeTodoistLinks(input) {
|
|
52
|
+
if (!input.includes('(') || !input.includes(')')) {
|
|
53
|
+
return input;
|
|
54
|
+
}
|
|
55
|
+
function removeMarkdown(_match, _url, text) {
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
return input.replace(TODOIST_LINK, removeMarkdown);
|
|
59
|
+
}
|
|
60
|
+
function removeAppLinks(input) {
|
|
61
|
+
if (input.includes('gmail')) {
|
|
62
|
+
input = input.replace(GMAIL_LINK, (_match, _id, text) => text);
|
|
63
|
+
}
|
|
64
|
+
if (input.includes('outlook')) {
|
|
65
|
+
input = input.replace(OUTLOOK_LINK, (_match, _id, text) => text);
|
|
66
|
+
}
|
|
67
|
+
if (input.includes('thunderbird')) {
|
|
68
|
+
input = input.replace(THUNDERBIRD_LINK, (_match, text) => text);
|
|
69
|
+
}
|
|
70
|
+
return input;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sanitizes a string by removing Todoist's formatting syntax (e.g. bold, italic, code blocks, links).
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Removes bold/italic formatting
|
|
77
|
+
* getSanitizedContent('Some **bold** and *italic*') // 'Some bold and italic'
|
|
78
|
+
*
|
|
79
|
+
* // Removes markdown links
|
|
80
|
+
* getSanitizedContent('A [markdown](http://url.com) link') // 'A markdown link'
|
|
81
|
+
*
|
|
82
|
+
* // Removes app-specific links
|
|
83
|
+
* getSanitizedContent('A [[gmail=id, link from gmail]]') // 'A link from gmail'
|
|
84
|
+
*
|
|
85
|
+
* @param input - The string to sanitize
|
|
86
|
+
* @returns The sanitized string with all formatting removed
|
|
87
|
+
*/
|
|
88
|
+
export function getSanitizedContent(input) {
|
|
89
|
+
input = removeStyleFormatting(input);
|
|
90
|
+
input = removeCodeFormatting(input);
|
|
91
|
+
input = removeFakeSectionFormatting(input);
|
|
92
|
+
input = removeMarkdownLinks(input);
|
|
93
|
+
input = removeTodoistLinks(input);
|
|
94
|
+
input = removeAppLinks(input);
|
|
95
|
+
return input;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Takes an array of tasks and returns a new array with sanitized content
|
|
99
|
+
* added as 'sanitizedContent' property to each task.
|
|
100
|
+
*
|
|
101
|
+
* @see {@link getSanitizedContent}
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* const tasks = [{ content: '**Bold** task', ... }]
|
|
105
|
+
* getSanitizedTasks(tasks) // [{ content: '**Bold** task', sanitizedContent: 'Bold task', ... }]
|
|
106
|
+
*
|
|
107
|
+
* @param tasks - Array of Task objects to sanitize
|
|
108
|
+
* @returns Array of tasks with added sanitizedContent property
|
|
109
|
+
*/
|
|
110
|
+
export function getSanitizedTasks(tasks) {
|
|
111
|
+
return tasks.map((task) => (Object.assign(Object.assign({}, task), { sanitizedContent: getSanitizedContent(task.content) })));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Escapes text for use as a token in a Todoist filter query.
|
|
115
|
+
*
|
|
116
|
+
* Prefixes parentheses, pipes, ampersands, exclamation marks, commas, and
|
|
117
|
+
* backslashes with a backslash. Quotes and asterisks are left unchanged.
|
|
118
|
+
* Whitespace validation and trimming are the caller's responsibility.
|
|
119
|
+
* Pass unescaped text to this function once, before inserting it into a query.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const tasks = await api.getTasksByFilter({
|
|
123
|
+
* query: `search: ${escapeFilterToken(searchText)}`,
|
|
124
|
+
* })
|
|
125
|
+
*
|
|
126
|
+
* @param text - The text to escape, not a complete filter query
|
|
127
|
+
* @returns The escaped text, with whitespace preserved
|
|
128
|
+
*/
|
|
129
|
+
export function escapeFilterToken(text) {
|
|
130
|
+
return text.replace(/[()|&!,\\]/g, '\\$&');
|
|
131
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a strongly-typed Sync API command with an auto-generated UUID.
|
|
4
|
+
*
|
|
5
|
+
* @param type - The command type (e.g. `'item_add'`, `'project_update'`).
|
|
6
|
+
* @param args - The command arguments, constrained by the command type.
|
|
7
|
+
* @param tempId - Optional temporary ID for referencing the created resource in subsequent commands.
|
|
8
|
+
* @returns A fully formed `SyncCommand` ready to include in a sync request.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const cmd = createCommand('item_add', { content: 'Buy milk', priority: 2 })
|
|
13
|
+
* // cmd.type === 'item_add'
|
|
14
|
+
* // cmd.args is typed as TaskAddArgs
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function createCommand(type, args, tempId) {
|
|
18
|
+
return Object.assign({ type, uuid: uuidv4(), args: args }, (tempId !== undefined ? { tempId } : {}));
|
|
19
|
+
}
|