@doist/todoist-api-typescript 6.0.1 → 6.1.5

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.
Files changed (34) hide show
  1. package/README.md +105 -5
  2. package/dist/cjs/authentication.js +59 -63
  3. package/dist/cjs/rest-client.js +18 -11
  4. package/dist/cjs/test-utils/mocks.js +2 -45
  5. package/dist/cjs/test-utils/msw-setup.js +74 -5
  6. package/dist/cjs/test-utils/obsidian-fetch-adapter.js +53 -0
  7. package/dist/cjs/todoist-api.js +80 -30
  8. package/dist/cjs/types/entities.js +4 -4
  9. package/dist/cjs/types/index.js +1 -0
  10. package/dist/cjs/utils/fetch-with-retry.js +37 -14
  11. package/dist/cjs/utils/multipart-upload.js +2 -1
  12. package/dist/esm/authentication.js +59 -63
  13. package/dist/esm/rest-client.js +18 -11
  14. package/dist/esm/test-utils/mocks.js +3 -10
  15. package/dist/esm/test-utils/msw-setup.js +67 -2
  16. package/dist/esm/test-utils/obsidian-fetch-adapter.js +50 -0
  17. package/dist/esm/todoist-api.js +80 -30
  18. package/dist/esm/types/entities.js +4 -4
  19. package/dist/esm/types/index.js +1 -0
  20. package/dist/esm/utils/fetch-with-retry.js +37 -14
  21. package/dist/esm/utils/multipart-upload.js +2 -1
  22. package/dist/types/authentication.d.ts +26 -3
  23. package/dist/types/rest-client.d.ts +2 -1
  24. package/dist/types/test-utils/mocks.d.ts +0 -1
  25. package/dist/types/test-utils/msw-setup.d.ts +31 -1
  26. package/dist/types/test-utils/obsidian-fetch-adapter.d.ts +29 -0
  27. package/dist/types/todoist-api.d.ts +19 -7
  28. package/dist/types/types/entities.d.ts +4 -4
  29. package/dist/types/types/http.d.ts +17 -0
  30. package/dist/types/types/index.d.ts +1 -0
  31. package/dist/types/types/sync.d.ts +5 -5
  32. package/dist/types/utils/fetch-with-retry.d.ts +2 -1
  33. package/dist/types/utils/multipart-upload.d.ts +2 -0
  34. package/package.json +8 -7
package/README.md CHANGED
@@ -38,6 +38,78 @@ Key changes in v1 include:
38
38
  - Object renames (e.g., items → tasks, notes → comments)
39
39
  - URL renames and endpoint signature changes
40
40
 
41
+ ## Custom HTTP Clients
42
+
43
+ The Todoist API client supports custom HTTP implementations to enable usage in environments with specific networking requirements, such as:
44
+
45
+ - **Obsidian plugins** - Desktop app with strict CORS policies
46
+ - **Browser extensions** - Custom HTTP APIs with different security models
47
+ - **Electron apps** - Requests routed through IPC layer
48
+ - **React Native** - Different networking stack
49
+ - **Enterprise environments** - Proxy configuration, custom headers, or certificate handling
50
+
51
+ ### Basic Usage
52
+
53
+ ```typescript
54
+ import { TodoistApi } from '@doist/todoist-api-typescript'
55
+
56
+ // Using the new options-based constructor
57
+ const api = new TodoistApi('YOURTOKEN', {
58
+ baseUrl: 'https://custom-api.example.com', // optional
59
+ customFetch: myCustomFetch, // your custom fetch implementation
60
+ })
61
+
62
+ // Legacy constructor (deprecated but supported)
63
+ const apiLegacy = new TodoistApi('YOURTOKEN', 'https://custom-api.example.com')
64
+ ```
65
+
66
+ ### Custom Fetch Interface
67
+
68
+ Your custom fetch function must implement this interface:
69
+
70
+ ```typescript
71
+ type CustomFetch = (
72
+ url: string,
73
+ options?: RequestInit & { timeout?: number },
74
+ ) => Promise<CustomFetchResponse>
75
+
76
+ type CustomFetchResponse = {
77
+ ok: boolean
78
+ status: number
79
+ statusText: string
80
+ headers: Record<string, string>
81
+ text(): Promise<string>
82
+ json(): Promise<unknown>
83
+ }
84
+ ```
85
+
86
+ ### OAuth with Custom Fetch
87
+
88
+ OAuth authentication functions (`getAuthToken`, `revokeAuthToken`, `revokeToken`) support custom fetch through an options object:
89
+
90
+ ```typescript
91
+ // New options-based usage
92
+ const { accessToken } = await getAuthToken(args, {
93
+ baseUrl: 'https://custom-auth.example.com',
94
+ customFetch: myCustomFetch,
95
+ })
96
+
97
+ await revokeToken(args, {
98
+ customFetch: myCustomFetch,
99
+ })
100
+
101
+ // Legacy usage (deprecated)
102
+ const { accessToken } = await getAuthToken(args, baseUrl)
103
+ ```
104
+
105
+ ### Important Notes
106
+
107
+ - All existing transforms (snake_case ↔ camelCase) work automatically with custom fetch
108
+ - Retry logic and error handling are preserved
109
+ - File uploads work with custom fetch implementations
110
+ - The custom fetch function should handle FormData for multipart uploads
111
+ - Timeout parameter is optional and up to your custom implementation
112
+
41
113
  ## Development and Testing
42
114
 
43
115
  Instead of having an example app in the repository to assist development and testing, we have included [ts-node](https://github.com/TypeStrong/ts-node) as a dev dependency. This allows us to have a scratch file locally that can import and utilize the API while developing or reviewing pull requests without having to manage a separate app project.
@@ -65,15 +137,43 @@ api.getProjects()
65
137
 
66
138
  ## Releases
67
139
 
68
- A new version is published to the NPM Registry whenever a new release on GitHub is created.
140
+ This project uses [Release Please](https://github.com/googleapis/release-please) to automate releases. Releases are created automatically based on [Conventional Commits](https://www.conventionalcommits.org/).
141
+
142
+ ### For Contributors
143
+
144
+ When making changes, use conventional commit messages:
145
+
146
+ - `feat:` - New features (triggers a minor version bump)
147
+ - `fix:` - Bug fixes (triggers a patch version bump)
148
+ - `feat!:` or `BREAKING CHANGE:` - Breaking changes (triggers a major version bump)
149
+ - `chore:`, `docs:`, `refactor:`, `perf:` - Other changes (included in changelog)
150
+
151
+ Example:
152
+
153
+ ```
154
+ feat: add support for recurring tasks
155
+ fix: resolve issue with date parsing
156
+ feat!: remove deprecated getTask method
157
+ ```
158
+
159
+ ### For Maintainers
160
+
161
+ The release process is fully automated:
162
+
163
+ 1. **Automatic PR Creation**: When commits are merged to `main`, Release Please automatically creates or updates a release PR with:
69
164
 
70
- The version in both package.json and package-lock.json is updated with:
165
+ - Updated version in `package.json`
166
+ - Updated `CHANGELOG.md`
167
+ - Aggregated changes since the last release
71
168
 
72
- `npm version <major|minor|patch> --no-git-tag-version`
169
+ 2. **Review and Merge**: Review the release PR to ensure the version bump and changelog are correct, then merge it.
73
170
 
74
- Once these changes have been pushed and merged, a release should be created, and a GitHub Action will automatically perform all the necessary steps and will release the version number that's specified inside the `package.json` file's version field.
171
+ 3. **Automatic Release**: Upon merging the release PR:
172
+ - A GitHub release is automatically created with the new version tag
173
+ - The `publish.yml` workflow is triggered by the tag
174
+ - The package is automatically published to NPM
75
175
 
76
- Users of the API client can then update to this version in their `package.json`.
176
+ Users of the API client can then update to the new version in their `package.json`.
77
177
 
78
178
  ### Feedback
79
179
 
@@ -58,81 +58,76 @@ function getAuthorizationUrl({ clientId, permissions, state, baseUrl, }) {
58
58
  const scope = permissions.join(',');
59
59
  return `${(0, endpoints_1.getAuthBaseUri)(baseUrl)}${endpoints_1.ENDPOINT_AUTHORIZATION}?client_id=${clientId}&scope=${scope}&state=${state}`;
60
60
  }
61
- /**
62
- * Exchanges an authorization code for an access token.
63
- *
64
- * @example
65
- * ```typescript
66
- * const { accessToken } = await getAuthToken({
67
- * clientId: 'your-client-id',
68
- * clientSecret: 'your-client-secret',
69
- * code: authCode
70
- * })
71
- * ```
72
- *
73
- * @returns The access token response
74
- * @throws {@link TodoistRequestError} If the token exchange fails
75
- */
76
- async function getAuthToken(args, baseUrl) {
61
+ async function getAuthToken(args, baseUrlOrOptions) {
77
62
  var _a;
78
- const response = await (0, rest_client_1.request)({
79
- httpMethod: 'POST',
80
- baseUri: (0, endpoints_1.getAuthBaseUri)(baseUrl),
81
- relativePath: endpoints_1.ENDPOINT_GET_TOKEN,
82
- apiToken: undefined,
83
- payload: args,
84
- });
85
- if (response.status !== 200 || !((_a = response.data) === null || _a === void 0 ? void 0 : _a.accessToken)) {
86
- throw new types_1.TodoistRequestError('Authentication token exchange failed.', response.status, response.data);
63
+ let baseUrl;
64
+ let customFetch;
65
+ if (typeof baseUrlOrOptions === 'string') {
66
+ // Legacy signature: (args, baseUrl)
67
+ baseUrl = baseUrlOrOptions;
68
+ customFetch = undefined;
69
+ }
70
+ else if (baseUrlOrOptions) {
71
+ // New signature: (args, options)
72
+ baseUrl = baseUrlOrOptions.baseUrl;
73
+ customFetch = baseUrlOrOptions.customFetch;
74
+ }
75
+ try {
76
+ const response = await (0, rest_client_1.request)({
77
+ httpMethod: 'POST',
78
+ baseUri: (0, endpoints_1.getAuthBaseUri)(baseUrl),
79
+ relativePath: endpoints_1.ENDPOINT_GET_TOKEN,
80
+ apiToken: undefined,
81
+ payload: args,
82
+ customFetch,
83
+ });
84
+ if (response.status !== 200 || !((_a = response.data) === null || _a === void 0 ? void 0 : _a.accessToken)) {
85
+ throw new types_1.TodoistRequestError('Authentication token exchange failed.', response.status, response.data);
86
+ }
87
+ return response.data;
88
+ }
89
+ catch (error) {
90
+ // Re-throw with custom message for authentication failures
91
+ const err = error;
92
+ throw new types_1.TodoistRequestError('Authentication token exchange failed.', err.httpStatusCode, err.responseData);
87
93
  }
88
- return response.data;
89
94
  }
90
- /**
91
- * Revokes an access token, making it invalid for future use.
92
- *
93
- * @example
94
- * ```typescript
95
- * await revokeAuthToken({
96
- * clientId: 'your-client-id',
97
- * clientSecret: 'your-client-secret',
98
- * accessToken: token
99
- * })
100
- * ```
101
- *
102
- * @deprecated Use {@link revokeToken} instead. This function uses a legacy endpoint that will be removed in a future version. The new function uses the RFC 7009 compliant endpoint.
103
- * @returns True if revocation was successful
104
- * @see https://todoist.com/api/v1/docs#tag/Authorization/operation/revoke_access_token_api_api_v1_access_tokens_delete
105
- */
106
- async function revokeAuthToken(args, baseUrl) {
95
+ async function revokeAuthToken(args, baseUrlOrOptions) {
96
+ let baseUrl;
97
+ let customFetch;
98
+ if (typeof baseUrlOrOptions === 'string') {
99
+ // Legacy signature: (args, baseUrl)
100
+ baseUrl = baseUrlOrOptions;
101
+ customFetch = undefined;
102
+ }
103
+ else if (baseUrlOrOptions) {
104
+ // New signature: (args, options)
105
+ baseUrl = baseUrlOrOptions.baseUrl;
106
+ customFetch = baseUrlOrOptions.customFetch;
107
+ }
107
108
  const response = await (0, rest_client_1.request)({
108
109
  httpMethod: 'POST',
109
110
  baseUri: (0, endpoints_1.getSyncBaseUri)(baseUrl),
110
111
  relativePath: endpoints_1.ENDPOINT_REVOKE_TOKEN,
111
112
  apiToken: undefined,
112
113
  payload: args,
114
+ customFetch,
113
115
  });
114
116
  return (0, rest_client_1.isSuccess)(response);
115
117
  }
116
- /**
117
- * Revokes a token using the RFC 7009 OAuth 2.0 Token Revocation standard.
118
- *
119
- * This function uses HTTP Basic Authentication with client credentials and follows
120
- * the RFC 7009 specification for token revocation.
121
- *
122
- * @example
123
- * ```typescript
124
- * await revokeToken({
125
- * clientId: 'your-client-id',
126
- * clientSecret: 'your-client-secret',
127
- * token: 'access-token-to-revoke'
128
- * })
129
- * ```
130
- *
131
- * @returns True if revocation was successful
132
- * @see https://datatracker.ietf.org/doc/html/rfc7009
133
- * @see https://todoist.com/api/v1/docs#tag/Authorization
134
- */
135
- async function revokeToken(args, baseUrl) {
118
+ async function revokeToken(args, baseUrlOrOptions) {
119
+ let baseUrl;
120
+ let customFetch;
121
+ if (typeof baseUrlOrOptions === 'string') {
122
+ // Legacy signature: (args, baseUrl)
123
+ baseUrl = baseUrlOrOptions;
124
+ customFetch = undefined;
125
+ }
126
+ else if (baseUrlOrOptions) {
127
+ // New signature: (args, options)
128
+ baseUrl = baseUrlOrOptions.baseUrl;
129
+ customFetch = baseUrlOrOptions.customFetch;
130
+ }
136
131
  const { clientId, clientSecret, token } = args;
137
132
  // Create Basic Auth header as per RFC 7009
138
133
  const basicAuth = createBasicAuthHeader(clientId, clientSecret);
@@ -153,6 +148,7 @@ async function revokeToken(args, baseUrl) {
153
148
  requestId: undefined,
154
149
  hasSyncCommands: false,
155
150
  customHeaders: customHeaders,
151
+ customFetch,
156
152
  });
157
153
  return (0, rest_client_1.isSuccess)(response);
158
154
  }
@@ -17,9 +17,14 @@ function paramsSerializer(params) {
17
17
  if (Array.isArray(value)) {
18
18
  qs.append(key, value.join(','));
19
19
  }
20
- else {
20
+ else if (typeof value === 'string' ||
21
+ typeof value === 'number' ||
22
+ typeof value === 'boolean') {
21
23
  qs.append(key, String(value));
22
24
  }
25
+ else {
26
+ qs.append(key, JSON.stringify(value));
27
+ }
23
28
  }
24
29
  });
25
30
  return qs.toString();
@@ -63,7 +68,7 @@ function isSuccess(response) {
63
68
  return response.status >= 200 && response.status < 300;
64
69
  }
65
70
  async function request(args) {
66
- const { httpMethod, baseUri, relativePath, apiToken, payload, requestId: initialRequestId, hasSyncCommands, customHeaders, } = args;
71
+ const { httpMethod, baseUri, relativePath, apiToken, payload, requestId: initialRequestId, hasSyncCommands, customHeaders, customFetch, } = args;
67
72
  // Capture original stack for better error reporting
68
73
  const originalStack = new Error();
69
74
  try {
@@ -94,24 +99,26 @@ async function request(args) {
94
99
  }
95
100
  break;
96
101
  case 'POST':
97
- case 'PUT': {
102
+ case 'PUT':
103
+ case 'DELETE': {
98
104
  // Convert payload from camelCase to snake_case
99
- const convertedPayload = payload ? (0, case_conversion_1.snakeCaseKeys)(payload) : payload;
100
- const body = hasSyncCommands
101
- ? JSON.stringify(convertedPayload)
102
- : JSON.stringify(convertedPayload);
103
- fetchOptions.body = body;
105
+ // Note: While DELETE with body is uncommon, the Todoist API uses it for some endpoints
106
+ if (payload) {
107
+ const convertedPayload = (0, case_conversion_1.snakeCaseKeys)(payload);
108
+ const body = hasSyncCommands
109
+ ? JSON.stringify(convertedPayload)
110
+ : JSON.stringify(convertedPayload);
111
+ fetchOptions.body = body;
112
+ }
104
113
  break;
105
114
  }
106
- case 'DELETE':
107
- // DELETE requests don't have a body
108
- break;
109
115
  }
110
116
  // Make the request
111
117
  const response = await (0, fetch_with_retry_1.fetchWithRetry)({
112
118
  url: finalUrl,
113
119
  options: fetchOptions,
114
120
  retryConfig: config.retry,
121
+ customFetch,
115
122
  });
116
123
  // Convert snake_case response to camelCase
117
124
  const convertedData = (0, case_conversion_1.camelCaseKeys)(response.data);
@@ -1,46 +1,3 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.setupRestClientMock = setupRestClientMock;
37
- const restClient = __importStar(require("../rest-client"));
38
- function setupRestClientMock(responseData, status = 200) {
39
- const response = {
40
- status,
41
- statusText: status === 200 ? 'OK' : 'Error',
42
- headers: {},
43
- data: responseData,
44
- };
45
- return jest.spyOn(restClient, 'request').mockResolvedValue(response);
46
- }
2
+ // This file is reserved for future test utilities
3
+ // All network mocking is now handled by MSW in msw-setup.ts
@@ -1,7 +1,43 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HttpResponse = exports.http = exports.server = exports.handlers = void 0;
4
+ exports.captureRequest = captureRequest;
5
+ exports.getLastRequest = getLastRequest;
6
+ exports.getAllRequests = getAllRequests;
7
+ exports.clearCapturedRequests = clearCapturedRequests;
8
+ exports.mockApiResponse = mockApiResponse;
9
+ exports.mockApiError = mockApiError;
4
10
  const node_1 = require("msw/node");
11
+ const msw_1 = require("msw");
12
+ Object.defineProperty(exports, "http", { enumerable: true, get: function () { return msw_1.http; } });
13
+ Object.defineProperty(exports, "HttpResponse", { enumerable: true, get: function () { return msw_1.HttpResponse; } });
14
+ // Request capture storage
15
+ const capturedRequests = [];
16
+ // Helper function to capture requests
17
+ function captureRequest({ request, body }) {
18
+ const headers = {};
19
+ request.headers.forEach((value, key) => {
20
+ headers[key] = value;
21
+ });
22
+ capturedRequests.push({
23
+ url: request.url,
24
+ method: request.method,
25
+ headers,
26
+ body,
27
+ });
28
+ }
29
+ // Helper function to get the last captured request
30
+ function getLastRequest() {
31
+ return capturedRequests[capturedRequests.length - 1];
32
+ }
33
+ // Helper function to get all captured requests
34
+ function getAllRequests() {
35
+ return [...capturedRequests];
36
+ }
37
+ // Helper function to clear captured requests
38
+ function clearCapturedRequests() {
39
+ capturedRequests.length = 0;
40
+ }
5
41
  // Default handlers for common API responses
6
42
  exports.handlers = [
7
43
  // Default handlers can be added here for common endpoints
@@ -9,19 +45,52 @@ exports.handlers = [
9
45
  ];
10
46
  // Create MSW server instance
11
47
  exports.server = (0, node_1.setupServer)(...exports.handlers);
48
+ // Helper to create a resolver function for MSW handlers
49
+ function createResolver(data, status, headers) {
50
+ return async function resolver({ request }) {
51
+ let body = undefined;
52
+ if (request.method !== 'GET') {
53
+ try {
54
+ body = await request.json();
55
+ }
56
+ catch (_a) {
57
+ // Body might not be JSON
58
+ try {
59
+ body = await request.text();
60
+ }
61
+ catch (_b) {
62
+ // Body might be FormData or not parseable
63
+ }
64
+ }
65
+ }
66
+ captureRequest({ request, body });
67
+ return msw_1.HttpResponse.json(data, {
68
+ status,
69
+ headers,
70
+ });
71
+ };
72
+ }
73
+ // Helper to mock a successful API response
74
+ function mockApiResponse({ endpoint, data, options = {}, }) {
75
+ const { status = 200, method = 'GET', headers = {} } = options;
76
+ const resolver = createResolver(data, status, headers);
77
+ const httpMethod = method.toLowerCase();
78
+ exports.server.use(msw_1.http[httpMethod](endpoint, resolver));
79
+ }
80
+ // Helper to mock an error response
81
+ function mockApiError({ endpoint, data, status, options = {}, }) {
82
+ mockApiResponse({ endpoint, data, options: Object.assign(Object.assign({}, options), { status }) });
83
+ }
12
84
  // Setup MSW for tests
13
85
  beforeAll(() => {
14
86
  exports.server.listen({
15
- onUnhandledRequest: 'warn', // Log warnings for unhandled requests during development
87
+ onUnhandledRequest: 'error', // Throw errors for unhandled requests to catch unexpected fetch calls
16
88
  });
17
89
  });
18
90
  afterEach(() => {
19
91
  exports.server.resetHandlers(); // Reset handlers between tests
92
+ clearCapturedRequests(); // Clear captured requests between tests
20
93
  });
21
94
  afterAll(() => {
22
95
  exports.server.close(); // Clean up after all tests
23
96
  });
24
- // Export MSW utilities for use in tests
25
- var msw_1 = require("msw");
26
- Object.defineProperty(exports, "http", { enumerable: true, get: function () { return msw_1.http; } });
27
- Object.defineProperty(exports, "HttpResponse", { enumerable: true, get: function () { return msw_1.HttpResponse; } });
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createObsidianFetchAdapter = createObsidianFetchAdapter;
4
+ /**
5
+ * Creates a CustomFetch adapter for Obsidian's requestUrl API.
6
+ *
7
+ * This adapter bridges the gap between Obsidian's requestUrl interface and the
8
+ * standard fetch-like interface expected by the Todoist API SDK.
9
+ *
10
+ * Key differences handled by this adapter:
11
+ * - Obsidian returns response data as properties (response.json, response.text)
12
+ * while the SDK expects methods (response.json(), response.text())
13
+ * - Obsidian's requestUrl bypasses CORS restrictions that would block standard fetch
14
+ * - Obsidian throws on HTTP errors by default; we set throw: false to handle manually
15
+ * - Obsidian doesn't provide statusText; we default to empty string
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { requestUrl } from 'obsidian';
20
+ * import { createObsidianFetchAdapter } from './obsidian-fetch-adapter';
21
+ *
22
+ * const api = new TodoistApi('your-token', {
23
+ * customFetch: createObsidianFetchAdapter(requestUrl)
24
+ * });
25
+ * ```
26
+ *
27
+ * @param requestUrl - The Obsidian requestUrl function
28
+ * @returns A CustomFetch function compatible with the Todoist API SDK
29
+ */
30
+ function createObsidianFetchAdapter(requestUrl) {
31
+ return async (url, options) => {
32
+ // Build the request parameters in Obsidian's format
33
+ const requestParams = {
34
+ url,
35
+ method: (options === null || options === void 0 ? void 0 : options.method) || 'GET',
36
+ headers: options === null || options === void 0 ? void 0 : options.headers,
37
+ body: options === null || options === void 0 ? void 0 : options.body,
38
+ throw: false, // Don't throw on HTTP errors; let the SDK handle status codes
39
+ };
40
+ // Make the request using Obsidian's requestUrl
41
+ const response = await requestUrl(requestParams);
42
+ // Transform Obsidian's response format to match CustomFetchResponse interface
43
+ return {
44
+ ok: response.status >= 200 && response.status < 300,
45
+ status: response.status,
46
+ statusText: '', // Obsidian doesn't provide statusText
47
+ headers: response.headers,
48
+ // Wrap Obsidian's direct properties as methods returning promises
49
+ text: () => Promise.resolve(response.text),
50
+ json: () => Promise.resolve(response.json),
51
+ };
52
+ };
53
+ }