@doist/comms-sdk 0.1.0-alpha.1
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 +143 -0
- package/dist/cjs/authentication.js +211 -0
- package/dist/cjs/clients/add-comment-helper.js +53 -0
- package/dist/cjs/clients/base-client.js +27 -0
- package/dist/cjs/clients/channels-client.js +83 -0
- package/dist/cjs/clients/comments-client.js +93 -0
- package/dist/cjs/clients/conversation-messages-client.js +87 -0
- package/dist/cjs/clients/conversations-client.js +103 -0
- package/dist/cjs/clients/groups-client.js +71 -0
- package/dist/cjs/clients/inbox-client.js +98 -0
- package/dist/cjs/clients/reactions-client.js +59 -0
- package/dist/cjs/clients/search-client.js +88 -0
- package/dist/cjs/clients/threads-client.js +135 -0
- package/dist/cjs/clients/users-client.js +199 -0
- package/dist/cjs/clients/workspace-users-client.js +140 -0
- package/dist/cjs/clients/workspaces-client.js +93 -0
- package/dist/cjs/comms-api.js +65 -0
- package/dist/cjs/consts/endpoints.js +27 -0
- package/dist/cjs/index.js +48 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/testUtils/msw-handlers.js +51 -0
- package/dist/cjs/testUtils/msw-setup.js +21 -0
- package/dist/cjs/testUtils/obsidian-fetch-adapter.js +53 -0
- package/dist/cjs/testUtils/test-defaults.js +102 -0
- package/dist/cjs/transport/fetch-with-retry.js +136 -0
- package/dist/cjs/transport/http-client.js +56 -0
- package/dist/cjs/transport/http-dispatcher.js +143 -0
- package/dist/cjs/types/entities.js +411 -0
- package/dist/cjs/types/enums.js +37 -0
- package/dist/cjs/types/errors.js +12 -0
- package/dist/cjs/types/http.js +4 -0
- package/dist/cjs/types/index.js +21 -0
- package/dist/cjs/types/requests.js +117 -0
- package/dist/cjs/utils/case-conversion.js +54 -0
- package/dist/cjs/utils/index.js +19 -0
- package/dist/cjs/utils/timestamp-conversion.js +49 -0
- package/dist/cjs/utils/url-helpers.js +131 -0
- package/dist/cjs/utils/uuidv7.js +174 -0
- package/dist/esm/authentication.js +203 -0
- package/dist/esm/clients/add-comment-helper.js +50 -0
- package/dist/esm/clients/base-client.js +23 -0
- package/dist/esm/clients/channels-client.js +79 -0
- package/dist/esm/clients/comments-client.js +89 -0
- package/dist/esm/clients/conversation-messages-client.js +83 -0
- package/dist/esm/clients/conversations-client.js +99 -0
- package/dist/esm/clients/groups-client.js +67 -0
- package/dist/esm/clients/inbox-client.js +94 -0
- package/dist/esm/clients/reactions-client.js +55 -0
- package/dist/esm/clients/search-client.js +84 -0
- package/dist/esm/clients/threads-client.js +131 -0
- package/dist/esm/clients/users-client.js +195 -0
- package/dist/esm/clients/workspace-users-client.js +136 -0
- package/dist/esm/clients/workspaces-client.js +89 -0
- package/dist/esm/comms-api.js +61 -0
- package/dist/esm/consts/endpoints.js +23 -0
- package/dist/esm/index.js +17 -0
- package/dist/esm/testUtils/msw-handlers.js +45 -0
- package/dist/esm/testUtils/msw-setup.js +18 -0
- package/dist/esm/testUtils/obsidian-fetch-adapter.js +50 -0
- package/dist/esm/testUtils/test-defaults.js +99 -0
- package/dist/esm/transport/fetch-with-retry.js +133 -0
- package/dist/esm/transport/http-client.js +51 -0
- package/dist/esm/transport/http-dispatcher.js +104 -0
- package/dist/esm/types/entities.js +408 -0
- package/dist/esm/types/enums.js +34 -0
- package/dist/esm/types/errors.js +8 -0
- package/dist/esm/types/http.js +1 -0
- package/dist/esm/types/index.js +5 -0
- package/dist/esm/types/requests.js +114 -0
- package/dist/esm/utils/case-conversion.js +47 -0
- package/dist/esm/utils/index.js +3 -0
- package/dist/esm/utils/timestamp-conversion.js +45 -0
- package/dist/esm/utils/url-helpers.js +112 -0
- package/dist/esm/utils/uuidv7.js +163 -0
- package/dist/types/authentication.d.ts +160 -0
- package/dist/types/clients/add-comment-helper.d.ts +12 -0
- package/dist/types/clients/base-client.d.ts +24 -0
- package/dist/types/clients/channels-client.d.ts +91 -0
- package/dist/types/clients/comments-client.d.ts +157 -0
- package/dist/types/clients/conversation-messages-client.d.ts +127 -0
- package/dist/types/clients/conversations-client.d.ts +206 -0
- package/dist/types/clients/groups-client.d.ts +55 -0
- package/dist/types/clients/inbox-client.d.ts +20 -0
- package/dist/types/clients/reactions-client.d.ts +19 -0
- package/dist/types/clients/search-client.d.ts +20 -0
- package/dist/types/clients/threads-client.d.ts +344 -0
- package/dist/types/clients/users-client.d.ts +123 -0
- package/dist/types/clients/workspace-users-client.d.ts +47 -0
- package/dist/types/clients/workspaces-client.d.ts +79 -0
- package/dist/types/comms-api.d.ts +59 -0
- package/dist/types/consts/endpoints.d.ts +19 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/testUtils/msw-handlers.d.ts +28 -0
- package/dist/types/testUtils/msw-setup.d.ts +1 -0
- package/dist/types/testUtils/obsidian-fetch-adapter.d.ts +29 -0
- package/dist/types/testUtils/test-defaults.d.ts +16 -0
- package/dist/types/transport/fetch-with-retry.d.ts +4 -0
- package/dist/types/transport/http-client.d.ts +13 -0
- package/dist/types/transport/http-dispatcher.d.ts +10 -0
- package/dist/types/types/entities.d.ts +1288 -0
- package/dist/types/types/enums.d.ts +55 -0
- package/dist/types/types/errors.d.ts +6 -0
- package/dist/types/types/http.d.ts +54 -0
- package/dist/types/types/index.d.ts +5 -0
- package/dist/types/types/requests.d.ts +385 -0
- package/dist/types/utils/case-conversion.d.ts +8 -0
- package/dist/types/utils/index.d.ts +3 -0
- package/dist/types/utils/timestamp-conversion.d.ts +13 -0
- package/dist/types/utils/url-helpers.d.ts +88 -0
- package/dist/types/utils/uuidv7.d.ts +40 -0
- package/package.json +93 -0
|
@@ -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 Comms 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 CommsApi('your-token', {
|
|
23
|
+
* customFetch: createObsidianFetchAdapter(requestUrl)
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @param requestUrl - The Obsidian requestUrl function
|
|
28
|
+
* @returns A CustomFetch function compatible with the Comms 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?.method || 'GET',
|
|
36
|
+
headers: options?.headers,
|
|
37
|
+
body: 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
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mockWorkspaceUser = exports.mockComment = exports.mockConversation = exports.mockGroup = exports.mockThread = exports.mockChannel = exports.mockWorkspace = exports.mockUser = exports.TEST_GROUP_ID = exports.TEST_MESSAGE_ID = exports.TEST_CONVERSATION_ID = exports.TEST_COMMENT_ID = exports.TEST_THREAD_ID = exports.TEST_CHANNEL_ID = exports.TEST_API_TOKEN = void 0;
|
|
4
|
+
exports.TEST_API_TOKEN = 'test-api-token';
|
|
5
|
+
// Canonical test IDs — shaped so they pass SDK-side validation.
|
|
6
|
+
exports.TEST_CHANNEL_ID = '7YpL3oZ4kZ9vP7Q1tR2sX3y';
|
|
7
|
+
exports.TEST_THREAD_ID = '7YpL3oZ4kZ9vP7Q1tR2sX3z';
|
|
8
|
+
exports.TEST_COMMENT_ID = '7YpL3oZ4kZ9vP7Q1tR2sX41';
|
|
9
|
+
exports.TEST_CONVERSATION_ID = '7YpL3oZ4kZ9vP7Q1tR2sX42';
|
|
10
|
+
exports.TEST_MESSAGE_ID = '7YpL3oZ4kZ9vP7Q1tR2sX43';
|
|
11
|
+
exports.TEST_GROUP_ID = '7YpL3oZ4kZ9vP7Q1tR2sX44';
|
|
12
|
+
exports.mockUser = {
|
|
13
|
+
id: 1,
|
|
14
|
+
email: 'test@example.com',
|
|
15
|
+
fullName: 'Test User',
|
|
16
|
+
shortName: 'TU',
|
|
17
|
+
timezone: 'America/New_York',
|
|
18
|
+
removed: false,
|
|
19
|
+
lang: 'en',
|
|
20
|
+
};
|
|
21
|
+
exports.mockWorkspace = {
|
|
22
|
+
id: 1,
|
|
23
|
+
name: 'Test Workspace',
|
|
24
|
+
creator: 1,
|
|
25
|
+
created: new Date('2021-01-01T00:00:00Z'),
|
|
26
|
+
};
|
|
27
|
+
exports.mockChannel = {
|
|
28
|
+
id: exports.TEST_CHANNEL_ID,
|
|
29
|
+
name: 'general',
|
|
30
|
+
creator: 1,
|
|
31
|
+
public: true,
|
|
32
|
+
workspaceId: 1,
|
|
33
|
+
archived: false,
|
|
34
|
+
created: new Date('2021-01-01T00:00:00Z'),
|
|
35
|
+
version: 0,
|
|
36
|
+
url: `https://comms.todoist.com/a/1/ch/${exports.TEST_CHANNEL_ID}/`,
|
|
37
|
+
};
|
|
38
|
+
exports.mockThread = {
|
|
39
|
+
id: exports.TEST_THREAD_ID,
|
|
40
|
+
title: 'Test Thread',
|
|
41
|
+
content: 'This is a test thread',
|
|
42
|
+
creator: 1,
|
|
43
|
+
channelId: exports.TEST_CHANNEL_ID,
|
|
44
|
+
workspaceId: 1,
|
|
45
|
+
commentCount: 0,
|
|
46
|
+
lastUpdated: new Date('2021-01-01T00:00:00Z'),
|
|
47
|
+
pinned: false,
|
|
48
|
+
posted: new Date('2021-01-01T00:00:00Z'),
|
|
49
|
+
snippet: 'This is a test thread',
|
|
50
|
+
snippetCreator: 1,
|
|
51
|
+
isArchived: false,
|
|
52
|
+
url: `https://comms.todoist.com/a/1/ch/${exports.TEST_CHANNEL_ID}/t/${exports.TEST_THREAD_ID}/`,
|
|
53
|
+
};
|
|
54
|
+
exports.mockGroup = {
|
|
55
|
+
id: exports.TEST_GROUP_ID,
|
|
56
|
+
name: 'Test Group',
|
|
57
|
+
workspaceId: 1,
|
|
58
|
+
userIds: [1, 2, 3],
|
|
59
|
+
version: 0,
|
|
60
|
+
};
|
|
61
|
+
exports.mockConversation = {
|
|
62
|
+
id: exports.TEST_CONVERSATION_ID,
|
|
63
|
+
workspaceId: 1,
|
|
64
|
+
userIds: [1, 2],
|
|
65
|
+
messageCount: 1,
|
|
66
|
+
lastObjIndex: 0,
|
|
67
|
+
snippet: 'Hello there',
|
|
68
|
+
snippetCreators: [1],
|
|
69
|
+
lastActive: new Date('2021-01-01T00:00:00Z'),
|
|
70
|
+
archived: false,
|
|
71
|
+
created: new Date('2021-01-01T00:00:00Z'),
|
|
72
|
+
creator: 1,
|
|
73
|
+
url: `https://comms.todoist.com/a/1/msg/${exports.TEST_CONVERSATION_ID}/`,
|
|
74
|
+
};
|
|
75
|
+
exports.mockComment = {
|
|
76
|
+
id: exports.TEST_COMMENT_ID,
|
|
77
|
+
content: 'This is a comment',
|
|
78
|
+
creator: 1,
|
|
79
|
+
threadId: exports.TEST_THREAD_ID,
|
|
80
|
+
workspaceId: 1,
|
|
81
|
+
channelId: exports.TEST_CHANNEL_ID,
|
|
82
|
+
posted: new Date('2021-01-01T00:00:00Z'),
|
|
83
|
+
url: `https://comms.todoist.com/a/1/ch/${exports.TEST_CHANNEL_ID}/t/${exports.TEST_THREAD_ID}/c/${exports.TEST_COMMENT_ID}`,
|
|
84
|
+
};
|
|
85
|
+
exports.mockWorkspaceUser = {
|
|
86
|
+
id: 1,
|
|
87
|
+
fullName: 'Test User',
|
|
88
|
+
email: 'test@example.com',
|
|
89
|
+
userType: 'USER',
|
|
90
|
+
shortName: 'TU',
|
|
91
|
+
firstName: 'Test',
|
|
92
|
+
imageId: null,
|
|
93
|
+
avatarUrls: null,
|
|
94
|
+
dateFormat: null,
|
|
95
|
+
removed: false,
|
|
96
|
+
restricted: null,
|
|
97
|
+
setupPending: null,
|
|
98
|
+
theme: null,
|
|
99
|
+
timeFormat: null,
|
|
100
|
+
timezone: 'America/New_York',
|
|
101
|
+
version: 1,
|
|
102
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchWithRetry = fetchWithRetry;
|
|
4
|
+
const errors_1 = require("../types/errors");
|
|
5
|
+
const case_conversion_1 = require("../utils/case-conversion");
|
|
6
|
+
const timestamp_conversion_1 = require("../utils/timestamp-conversion");
|
|
7
|
+
const http_dispatcher_1 = require("./http-dispatcher");
|
|
8
|
+
async function fetchWithRetry(url, options, maxRetries = 3, customFetch) {
|
|
9
|
+
let lastError;
|
|
10
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
11
|
+
let clearTimeoutFn;
|
|
12
|
+
try {
|
|
13
|
+
let requestSignal = options.signal || undefined;
|
|
14
|
+
if (options.timeout && options.timeout > 0) {
|
|
15
|
+
const timeoutResult = createTimeoutSignal(options.timeout, requestSignal);
|
|
16
|
+
requestSignal = timeoutResult.signal;
|
|
17
|
+
clearTimeoutFn = timeoutResult.clear;
|
|
18
|
+
}
|
|
19
|
+
const response = customFetch
|
|
20
|
+
? await customFetch(url, options)
|
|
21
|
+
: await fetchWithDefaultTransport(url, options, requestSignal);
|
|
22
|
+
const responseText = await response.text();
|
|
23
|
+
let responseData;
|
|
24
|
+
try {
|
|
25
|
+
responseData = responseText ? JSON.parse(responseText) : undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
responseData = responseText;
|
|
29
|
+
}
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new errors_1.CommsRequestError(`Request failed with status ${response.status}`, response.status, responseData);
|
|
32
|
+
}
|
|
33
|
+
const camelCased = (0, case_conversion_1.camelCaseKeys)(responseData);
|
|
34
|
+
const transformed = (0, timestamp_conversion_1.transformTimestamps)(camelCased);
|
|
35
|
+
if (clearTimeoutFn) {
|
|
36
|
+
clearTimeoutFn();
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
data: transformed,
|
|
40
|
+
status: response.status,
|
|
41
|
+
headers: response.headers,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (clearTimeoutFn) {
|
|
46
|
+
clearTimeoutFn();
|
|
47
|
+
}
|
|
48
|
+
lastError = error instanceof Error ? error : new Error('Unknown error');
|
|
49
|
+
if (attempt < maxRetries && isNetworkError(lastError)) {
|
|
50
|
+
const delay = getRetryDelay(attempt + 1);
|
|
51
|
+
if (delay > 0) {
|
|
52
|
+
await sleep(delay);
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (lastError instanceof errors_1.CommsRequestError) {
|
|
60
|
+
throw lastError;
|
|
61
|
+
}
|
|
62
|
+
throw new errors_1.CommsRequestError(lastError?.message ?? 'Request failed');
|
|
63
|
+
}
|
|
64
|
+
async function fetchWithDefaultTransport(url, options, signal) {
|
|
65
|
+
const dispatcher = await (0, http_dispatcher_1.getDefaultDispatcher)();
|
|
66
|
+
const response = dispatcher
|
|
67
|
+
? await fetch(url, {
|
|
68
|
+
...options,
|
|
69
|
+
signal,
|
|
70
|
+
// @ts-expect-error - dispatcher is valid for Node.js fetch but not in TS types
|
|
71
|
+
dispatcher,
|
|
72
|
+
})
|
|
73
|
+
: await fetch(url, {
|
|
74
|
+
...options,
|
|
75
|
+
signal,
|
|
76
|
+
});
|
|
77
|
+
return convertResponseToCustomFetch(response);
|
|
78
|
+
}
|
|
79
|
+
async function sleep(ms) {
|
|
80
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
81
|
+
}
|
|
82
|
+
const timeoutErrorName = 'TimeoutError';
|
|
83
|
+
function createTimeoutError(timeoutMs) {
|
|
84
|
+
const error = new Error(`Request timeout after ${timeoutMs}ms`);
|
|
85
|
+
error.name = timeoutErrorName;
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
function isNetworkError(error) {
|
|
89
|
+
return (error.name === 'TypeError' ||
|
|
90
|
+
error.name === timeoutErrorName ||
|
|
91
|
+
error.message.toLowerCase().includes('network'));
|
|
92
|
+
}
|
|
93
|
+
function getRetryDelay(retryCount) {
|
|
94
|
+
return retryCount === 1 ? 0 : 500;
|
|
95
|
+
}
|
|
96
|
+
function createTimeoutSignal(timeoutMs, existingSignal) {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timeoutId = setTimeout(() => {
|
|
99
|
+
controller.abort(createTimeoutError(timeoutMs));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
let abortHandler;
|
|
102
|
+
function clear() {
|
|
103
|
+
clearTimeout(timeoutId);
|
|
104
|
+
if (existingSignal && abortHandler) {
|
|
105
|
+
existingSignal.removeEventListener('abort', abortHandler);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (existingSignal) {
|
|
109
|
+
if (existingSignal.aborted) {
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
controller.abort(existingSignal.reason);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
abortHandler = () => {
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
controller.abort(existingSignal.reason);
|
|
117
|
+
};
|
|
118
|
+
existingSignal.addEventListener('abort', abortHandler, { once: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { signal: controller.signal, clear };
|
|
122
|
+
}
|
|
123
|
+
function convertResponseToCustomFetch(response) {
|
|
124
|
+
const headers = {};
|
|
125
|
+
response.headers.forEach((value, key) => {
|
|
126
|
+
headers[key] = value;
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
ok: response.ok,
|
|
130
|
+
status: response.status,
|
|
131
|
+
statusText: response.statusText,
|
|
132
|
+
headers,
|
|
133
|
+
text: () => response.clone().text(),
|
|
134
|
+
json: () => response.json(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paramsSerializer = paramsSerializer;
|
|
4
|
+
exports.request = request;
|
|
5
|
+
exports.isSuccess = isSuccess;
|
|
6
|
+
const case_conversion_1 = require("../utils/case-conversion");
|
|
7
|
+
const fetch_with_retry_1 = require("./fetch-with-retry");
|
|
8
|
+
function paramsSerializer(params) {
|
|
9
|
+
const qs = new URLSearchParams();
|
|
10
|
+
Object.keys(params).forEach((key) => {
|
|
11
|
+
const value = params[key];
|
|
12
|
+
if (value != null) {
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
qs.append(key, value.join(','));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
qs.append(key, String(value));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return qs.toString();
|
|
22
|
+
}
|
|
23
|
+
const defaultHeaders = {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
};
|
|
26
|
+
function getAuthHeader(apiToken) {
|
|
27
|
+
return `Bearer ${apiToken}`;
|
|
28
|
+
}
|
|
29
|
+
function getRequestConfiguration(baseURL, apiToken, requestId) {
|
|
30
|
+
const authHeader = apiToken ? { Authorization: getAuthHeader(apiToken) } : undefined;
|
|
31
|
+
const requestIdHeader = requestId ? { 'X-Request-Id': requestId } : undefined;
|
|
32
|
+
const headers = { ...defaultHeaders, ...authHeader, ...requestIdHeader };
|
|
33
|
+
return { baseURL, headers, timeout: 30000 };
|
|
34
|
+
}
|
|
35
|
+
async function request(args) {
|
|
36
|
+
const { httpMethod, baseUri, relativePath, apiToken, payload, requestId, customFetch } = args;
|
|
37
|
+
const config = getRequestConfiguration(baseUri, apiToken, requestId);
|
|
38
|
+
const url = new URL(relativePath, config.baseURL).toString();
|
|
39
|
+
const options = {
|
|
40
|
+
method: httpMethod,
|
|
41
|
+
headers: config.headers,
|
|
42
|
+
timeout: config.timeout,
|
|
43
|
+
};
|
|
44
|
+
if (httpMethod === 'GET' && payload) {
|
|
45
|
+
const searchParams = paramsSerializer((0, case_conversion_1.snakeCaseKeys)(payload));
|
|
46
|
+
const urlWithParams = searchParams ? `${url}?${searchParams}` : url;
|
|
47
|
+
return (0, fetch_with_retry_1.fetchWithRetry)(urlWithParams, options, 3, customFetch);
|
|
48
|
+
}
|
|
49
|
+
if (payload && httpMethod !== 'GET') {
|
|
50
|
+
options.body = JSON.stringify((0, case_conversion_1.snakeCaseKeys)(payload));
|
|
51
|
+
}
|
|
52
|
+
return (0, fetch_with_retry_1.fetchWithRetry)(url, options, 3, customFetch);
|
|
53
|
+
}
|
|
54
|
+
function isSuccess(response) {
|
|
55
|
+
return response.status >= 200 && response.status < 300;
|
|
56
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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.getDefaultDispatcher = getDefaultDispatcher;
|
|
37
|
+
exports.closeDefaultDispatcher = closeDefaultDispatcher;
|
|
38
|
+
exports.resetDefaultDispatcherForTests = resetDefaultDispatcherForTests;
|
|
39
|
+
exports.suppressExperimentalWarningsSync = suppressExperimentalWarningsSync;
|
|
40
|
+
let defaultDispatcher;
|
|
41
|
+
let defaultDispatcherPromise;
|
|
42
|
+
async function getDefaultDispatcher() {
|
|
43
|
+
if (defaultDispatcher) {
|
|
44
|
+
return defaultDispatcher;
|
|
45
|
+
}
|
|
46
|
+
if (!defaultDispatcherPromise) {
|
|
47
|
+
defaultDispatcherPromise = createDefaultDispatcher()
|
|
48
|
+
.then((dispatcher) => {
|
|
49
|
+
defaultDispatcher = dispatcher;
|
|
50
|
+
return dispatcher;
|
|
51
|
+
})
|
|
52
|
+
.catch((error) => {
|
|
53
|
+
defaultDispatcher = undefined;
|
|
54
|
+
defaultDispatcherPromise = undefined;
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return defaultDispatcherPromise;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Drains the default dispatcher's connection pool. CLIs and scripts should
|
|
62
|
+
* `await` this before exit so Node's event loop empties immediately instead
|
|
63
|
+
* of waiting ~4s on keep-alive. No-op in the browser branch.
|
|
64
|
+
*/
|
|
65
|
+
async function closeDefaultDispatcher() {
|
|
66
|
+
// Clear the singleton *before* awaiting init, so any concurrent
|
|
67
|
+
// `getDefaultDispatcher()` after this point creates a fresh dispatcher
|
|
68
|
+
// instead of receiving a reference to the one we're about to close.
|
|
69
|
+
const initPromise = defaultDispatcherPromise;
|
|
70
|
+
defaultDispatcher = undefined;
|
|
71
|
+
defaultDispatcherPromise = undefined;
|
|
72
|
+
if (!initPromise)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
const dispatcher = await initPromise;
|
|
76
|
+
if (dispatcher) {
|
|
77
|
+
await dispatcher.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// init failed; nothing to close
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function resetDefaultDispatcherForTests() {
|
|
85
|
+
defaultDispatcher = undefined;
|
|
86
|
+
defaultDispatcherPromise = undefined;
|
|
87
|
+
}
|
|
88
|
+
function isNodeEnvironment() {
|
|
89
|
+
return typeof process !== 'undefined' && typeof process.versions?.node === 'string';
|
|
90
|
+
}
|
|
91
|
+
async function createDefaultDispatcher() {
|
|
92
|
+
if (!isNodeEnvironment()) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
96
|
+
// undici into their bundle. `isNodeEnvironment()` above already gated this
|
|
97
|
+
// branch, so undici is safe to load when we get here.
|
|
98
|
+
const { EnvHttpProxyAgent, interceptors } = await Promise.resolve().then(() => __importStar(require('undici')));
|
|
99
|
+
// `allowH2: true` opts into HTTP/2 via ALPN; undici falls back to h1.1
|
|
100
|
+
// when the server doesn't negotiate h2. Without this flag undici
|
|
101
|
+
// defaults to h1.1 even when the server supports h2.
|
|
102
|
+
//
|
|
103
|
+
// `interceptors.decompress()` decodes gzip/deflate/br/zstd bodies. On
|
|
104
|
+
// Node 24+, attaching any custom dispatcher to global `fetch` strips
|
|
105
|
+
// `content-encoding` without actually decompressing the body.
|
|
106
|
+
// See https://github.com/Doist/todoist-cli/issues/318.
|
|
107
|
+
//
|
|
108
|
+
// Both emit ExperimentalWarning on first use; suppressed during init.
|
|
109
|
+
return suppressExperimentalWarningsSync(() => {
|
|
110
|
+
const decompress = interceptors.decompress();
|
|
111
|
+
return new EnvHttpProxyAgent({ allowH2: true }).compose(decompress);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// `suppressExperimentalWarningsSync` is exported for direct unit testing —
|
|
115
|
+
// the integration path through `getDefaultDispatcher()` can't reliably
|
|
116
|
+
// exercise it because both the dispatcher singleton and undici's internal
|
|
117
|
+
// `warningEmitted` flag are once-per-process.
|
|
118
|
+
//
|
|
119
|
+
// `fn` must be synchronous so the override covers a single critical section
|
|
120
|
+
// (microseconds) — no unrelated `ExperimentalWarning` from elsewhere can
|
|
121
|
+
// interleave and be lost. We suppress every `ExperimentalWarning` rather than
|
|
122
|
+
// pattern-matching the message text: the message wording is an undici
|
|
123
|
+
// implementation detail (not a stable API), and the suppression window is
|
|
124
|
+
// narrow enough that a coarse type filter is safe.
|
|
125
|
+
function suppressExperimentalWarningsSync(fn) {
|
|
126
|
+
const originalEmit = process.emitWarning;
|
|
127
|
+
process.emitWarning = ((warning, typeOrOptions, ...rest) => {
|
|
128
|
+
const type = typeof typeOrOptions === 'string'
|
|
129
|
+
? typeOrOptions
|
|
130
|
+
: typeof typeOrOptions === 'object' && typeOrOptions !== null
|
|
131
|
+
? typeOrOptions.type
|
|
132
|
+
: undefined;
|
|
133
|
+
if (type === 'ExperimentalWarning')
|
|
134
|
+
return;
|
|
135
|
+
originalEmit.call(process, warning, typeOrOptions, ...rest);
|
|
136
|
+
});
|
|
137
|
+
try {
|
|
138
|
+
return fn();
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
process.emitWarning = originalEmit;
|
|
142
|
+
}
|
|
143
|
+
}
|