@doist/todoist-api-typescript 6.0.0 → 6.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -5
- package/dist/cjs/authentication.js +59 -63
- package/dist/cjs/rest-client.js +15 -11
- package/dist/cjs/test-utils/mocks.js +2 -45
- package/dist/cjs/test-utils/msw-setup.js +74 -5
- package/dist/cjs/test-utils/obsidian-fetch-adapter.js +53 -0
- package/dist/cjs/todoist-api.js +80 -30
- package/dist/cjs/types/entities.js +4 -4
- package/dist/cjs/types/index.js +1 -0
- package/dist/cjs/utils/fetch-with-retry.js +37 -14
- package/dist/cjs/utils/multipart-upload.js +2 -1
- package/dist/esm/authentication.js +59 -63
- package/dist/esm/rest-client.js +15 -11
- package/dist/esm/test-utils/mocks.js +3 -10
- package/dist/esm/test-utils/msw-setup.js +67 -2
- package/dist/esm/test-utils/obsidian-fetch-adapter.js +50 -0
- package/dist/esm/todoist-api.js +80 -30
- package/dist/esm/types/entities.js +4 -4
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/fetch-with-retry.js +37 -14
- package/dist/esm/utils/multipart-upload.js +2 -1
- package/dist/types/authentication.d.ts +20 -0
- package/dist/types/rest-client.d.ts +2 -1
- package/dist/types/test-utils/mocks.d.ts +0 -1
- package/dist/types/test-utils/msw-setup.d.ts +31 -1
- package/dist/types/test-utils/obsidian-fetch-adapter.d.ts +29 -0
- package/dist/types/todoist-api.d.ts +18 -7
- package/dist/types/types/entities.d.ts +4 -4
- package/dist/types/types/http.d.ts +17 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/sync.d.ts +5 -5
- package/dist/types/utils/fetch-with-retry.d.ts +2 -1
- package/dist/types/utils/multipart-upload.d.ts +2 -0
- package/package.json +4 -3
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
|
-
|
|
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
|
-
|
|
165
|
+
- Updated version in `package.json`
|
|
166
|
+
- Updated `CHANGELOG.md`
|
|
167
|
+
- Aggregated changes since the last release
|
|
71
168
|
|
|
72
|
-
|
|
169
|
+
2. **Review and Merge**: Review the release PR to ensure the version bump and changelog are correct, then merge it.
|
|
73
170
|
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
}
|
package/dist/cjs/rest-client.js
CHANGED
|
@@ -63,7 +63,7 @@ function isSuccess(response) {
|
|
|
63
63
|
return response.status >= 200 && response.status < 300;
|
|
64
64
|
}
|
|
65
65
|
async function request(args) {
|
|
66
|
-
const { httpMethod, baseUri, relativePath, apiToken, payload, requestId: initialRequestId, hasSyncCommands, customHeaders, } = args;
|
|
66
|
+
const { httpMethod, baseUri, relativePath, apiToken, payload, requestId: initialRequestId, hasSyncCommands, customHeaders, customFetch, } = args;
|
|
67
67
|
// Capture original stack for better error reporting
|
|
68
68
|
const originalStack = new Error();
|
|
69
69
|
try {
|
|
@@ -84,7 +84,9 @@ async function request(args) {
|
|
|
84
84
|
case 'GET':
|
|
85
85
|
// For GET requests, add query parameters to URL
|
|
86
86
|
if (payload) {
|
|
87
|
-
|
|
87
|
+
// Convert payload from camelCase to snake_case
|
|
88
|
+
const convertedPayload = (0, case_conversion_1.snakeCaseKeys)(payload);
|
|
89
|
+
const queryString = paramsSerializer(convertedPayload);
|
|
88
90
|
if (queryString) {
|
|
89
91
|
const separator = url.includes('?') ? '&' : '?';
|
|
90
92
|
finalUrl = `${url}${separator}${queryString}`;
|
|
@@ -92,24 +94,26 @@ async function request(args) {
|
|
|
92
94
|
}
|
|
93
95
|
break;
|
|
94
96
|
case 'POST':
|
|
95
|
-
case 'PUT':
|
|
97
|
+
case 'PUT':
|
|
98
|
+
case 'DELETE': {
|
|
96
99
|
// Convert payload from camelCase to snake_case
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// Note: While DELETE with body is uncommon, the Todoist API uses it for some endpoints
|
|
101
|
+
if (payload) {
|
|
102
|
+
const convertedPayload = (0, case_conversion_1.snakeCaseKeys)(payload);
|
|
103
|
+
const body = hasSyncCommands
|
|
104
|
+
? JSON.stringify(convertedPayload)
|
|
105
|
+
: JSON.stringify(convertedPayload);
|
|
106
|
+
fetchOptions.body = body;
|
|
107
|
+
}
|
|
102
108
|
break;
|
|
103
109
|
}
|
|
104
|
-
case 'DELETE':
|
|
105
|
-
// DELETE requests don't have a body
|
|
106
|
-
break;
|
|
107
110
|
}
|
|
108
111
|
// Make the request
|
|
109
112
|
const response = await (0, fetch_with_retry_1.fetchWithRetry)({
|
|
110
113
|
url: finalUrl,
|
|
111
114
|
options: fetchOptions,
|
|
112
115
|
retryConfig: config.retry,
|
|
116
|
+
customFetch,
|
|
113
117
|
});
|
|
114
118
|
// Convert snake_case response to camelCase
|
|
115
119
|
const convertedData = (0, case_conversion_1.camelCaseKeys)(response.data);
|
|
@@ -1,46 +1,3 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
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: '
|
|
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
|
+
}
|