@doist/todoist-api-typescript 5.8.0 → 6.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/README.md +1 -1
- package/dist/cjs/authentication.js +158 -0
- package/dist/cjs/consts/endpoints.js +74 -0
- package/dist/{index.js → cjs/index.js} +1 -1
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/rest-client.js +124 -0
- package/dist/{testUtils → cjs/test-utils}/asserts.js +1 -1
- package/dist/{testUtils → cjs/test-utils}/mocks.js +8 -4
- package/dist/cjs/test-utils/msw-setup.js +27 -0
- package/dist/{testUtils/testDefaults.js → cjs/test-utils/test-defaults.js} +41 -52
- package/dist/cjs/todoist-api.js +1235 -0
- package/dist/{types → cjs/types}/entities.js +78 -31
- package/dist/cjs/types/errors.js +22 -0
- package/dist/cjs/types/http.js +22 -0
- package/dist/cjs/utils/case-conversion.js +69 -0
- package/dist/{utils → cjs/utils}/colors.js +3 -3
- package/dist/cjs/utils/fetch-with-retry.js +150 -0
- package/dist/{utils → cjs/utils}/index.js +4 -4
- package/dist/cjs/utils/multipart-upload.js +126 -0
- package/dist/{utils → cjs/utils}/processing-helpers.js +3 -3
- package/dist/{utils → cjs/utils}/sanitization.js +17 -28
- package/dist/{utils/urlHelpers.js → cjs/utils/url-helpers.js} +15 -15
- package/dist/{utils → cjs/utils}/validators.js +28 -1
- package/dist/esm/authentication.js +151 -0
- package/dist/esm/consts/endpoints.js +65 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/rest-client.js +119 -0
- package/dist/esm/test-utils/asserts.js +8 -0
- package/dist/esm/test-utils/mocks.js +10 -0
- package/dist/esm/test-utils/msw-setup.js +22 -0
- package/dist/esm/test-utils/test-defaults.js +198 -0
- package/dist/esm/todoist-api.js +1231 -0
- package/dist/esm/types/entities.js +366 -0
- package/dist/esm/types/errors.js +18 -0
- package/dist/esm/types/http.js +18 -0
- package/dist/esm/types/index.js +3 -0
- package/dist/esm/types/requests.js +1 -0
- package/dist/esm/types/sync.js +1 -0
- package/dist/esm/utils/activity-helpers.js +36 -0
- package/dist/esm/utils/case-conversion.js +61 -0
- package/dist/esm/utils/colors.js +215 -0
- package/dist/esm/utils/fetch-with-retry.js +147 -0
- package/dist/esm/utils/index.js +3 -0
- package/dist/esm/utils/multipart-upload.js +120 -0
- package/dist/esm/utils/processing-helpers.js +12 -0
- package/dist/esm/utils/sanitization.js +112 -0
- package/dist/esm/utils/url-helpers.js +68 -0
- package/dist/esm/utils/validators.js +97 -0
- package/dist/{authentication.d.ts → types/authentication.d.ts} +6 -1
- package/dist/{consts → types/consts}/endpoints.d.ts +11 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/rest-client.d.ts +15 -0
- package/dist/types/test-utils/msw-setup.d.ts +3 -0
- package/dist/{TodoistApi.d.ts → types/todoist-api.d.ts} +91 -2
- package/dist/types/{entities.d.ts → types/entities.d.ts} +119 -0
- package/dist/types/types/http.d.ts +68 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/{requests.d.ts → types/requests.d.ts} +137 -0
- package/dist/types/utils/case-conversion.d.ts +12 -0
- package/dist/types/utils/fetch-with-retry.d.ts +11 -0
- package/dist/types/utils/index.d.ts +3 -0
- package/dist/types/utils/multipart-upload.d.ts +50 -0
- package/dist/{utils → types/utils}/validators.d.ts +7 -1
- package/package.json +24 -8
- package/dist/TodoistApi.js +0 -1209
- package/dist/authentication.js +0 -199
- package/dist/consts/endpoints.js +0 -50
- package/dist/index.d.ts +0 -4
- package/dist/restClient.d.ts +0 -5
- package/dist/restClient.js +0 -170
- package/dist/types/errors.js +0 -39
- package/dist/types/http.d.ts +0 -1
- package/dist/types/http.js +0 -2
- package/dist/utils/index.d.ts +0 -3
- /package/dist/{types → cjs/types}/index.js +0 -0
- /package/dist/{types → cjs/types}/requests.js +0 -0
- /package/dist/{types → cjs/types}/sync.js +0 -0
- /package/dist/{utils → cjs/utils}/activity-helpers.js +0 -0
- /package/dist/{testUtils → types/test-utils}/asserts.d.ts +0 -0
- /package/dist/{testUtils → types/test-utils}/mocks.d.ts +0 -0
- /package/dist/{testUtils/testDefaults.d.ts → types/test-utils/test-defaults.d.ts} +0 -0
- /package/dist/types/{errors.d.ts → types/errors.d.ts} +0 -0
- /package/dist/types/{sync.d.ts → types/sync.d.ts} +0 -0
- /package/dist/{utils → types/utils}/activity-helpers.d.ts +0 -0
- /package/dist/{utils → types/utils}/colors.d.ts +0 -0
- /package/dist/{utils → types/utils}/processing-helpers.d.ts +0 -0
- /package/dist/{utils → types/utils}/sanitization.d.ts +0 -0
- /package/dist/{utils/urlHelpers.d.ts → types/utils/url-helpers.d.ts} +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export const berryRed = {
|
|
2
|
+
id: 30,
|
|
3
|
+
key: 'berry_red',
|
|
4
|
+
displayName: 'Berry Red',
|
|
5
|
+
name: 'Berry Red',
|
|
6
|
+
hexValue: '#b8255f',
|
|
7
|
+
value: '#b8255f',
|
|
8
|
+
};
|
|
9
|
+
export const red = {
|
|
10
|
+
id: 31,
|
|
11
|
+
key: 'red',
|
|
12
|
+
displayName: 'Red',
|
|
13
|
+
name: 'Red',
|
|
14
|
+
hexValue: '#db4035',
|
|
15
|
+
value: '#db4035',
|
|
16
|
+
};
|
|
17
|
+
export const orange = {
|
|
18
|
+
id: 32,
|
|
19
|
+
key: 'orange',
|
|
20
|
+
displayName: 'Orange',
|
|
21
|
+
name: 'Orange',
|
|
22
|
+
hexValue: '#ff9933',
|
|
23
|
+
value: '#ff9933',
|
|
24
|
+
};
|
|
25
|
+
export const yellow = {
|
|
26
|
+
id: 33,
|
|
27
|
+
key: 'yellow',
|
|
28
|
+
displayName: 'Yellow',
|
|
29
|
+
name: 'Yellow',
|
|
30
|
+
hexValue: '#fad000',
|
|
31
|
+
value: '#fad000',
|
|
32
|
+
};
|
|
33
|
+
export const oliveGreen = {
|
|
34
|
+
id: 34,
|
|
35
|
+
key: 'olive_green',
|
|
36
|
+
displayName: 'Olive Green',
|
|
37
|
+
name: 'Olive Green',
|
|
38
|
+
hexValue: '#afb83b',
|
|
39
|
+
value: '#afb83b',
|
|
40
|
+
};
|
|
41
|
+
export const limeGreen = {
|
|
42
|
+
id: 35,
|
|
43
|
+
key: 'lime_green',
|
|
44
|
+
displayName: 'Lime Green',
|
|
45
|
+
name: 'Lime Green',
|
|
46
|
+
hexValue: '#7ecc49',
|
|
47
|
+
value: '#7ecc49',
|
|
48
|
+
};
|
|
49
|
+
export const green = {
|
|
50
|
+
id: 36,
|
|
51
|
+
key: 'green',
|
|
52
|
+
displayName: 'Green',
|
|
53
|
+
name: 'Green',
|
|
54
|
+
hexValue: '#299438',
|
|
55
|
+
value: '#299438',
|
|
56
|
+
};
|
|
57
|
+
export const mintGreen = {
|
|
58
|
+
id: 37,
|
|
59
|
+
key: 'mint_green',
|
|
60
|
+
displayName: 'Mint Green',
|
|
61
|
+
name: 'Mint Green',
|
|
62
|
+
hexValue: '#6accbc',
|
|
63
|
+
value: '#6accbc',
|
|
64
|
+
};
|
|
65
|
+
export const turquoise = {
|
|
66
|
+
id: 38,
|
|
67
|
+
key: 'turquoise',
|
|
68
|
+
displayName: 'Turquoise',
|
|
69
|
+
name: 'Turquoise',
|
|
70
|
+
hexValue: '#158fad',
|
|
71
|
+
value: '#158fad',
|
|
72
|
+
};
|
|
73
|
+
export const skyBlue = {
|
|
74
|
+
id: 39,
|
|
75
|
+
key: 'sky_blue',
|
|
76
|
+
displayName: 'Sky Blue',
|
|
77
|
+
name: 'Sky Blue',
|
|
78
|
+
hexValue: '#14aaf5',
|
|
79
|
+
value: '#14aaf5',
|
|
80
|
+
};
|
|
81
|
+
export const lightBlue = {
|
|
82
|
+
id: 40,
|
|
83
|
+
key: 'light_blue',
|
|
84
|
+
displayName: 'Light Blue',
|
|
85
|
+
name: 'Light Blue',
|
|
86
|
+
hexValue: '#96c3eb',
|
|
87
|
+
value: '#96c3eb',
|
|
88
|
+
};
|
|
89
|
+
export const blue = {
|
|
90
|
+
id: 41,
|
|
91
|
+
key: 'blue',
|
|
92
|
+
displayName: 'Blue',
|
|
93
|
+
name: 'Blue',
|
|
94
|
+
hexValue: '#4073ff',
|
|
95
|
+
value: '#4073ff',
|
|
96
|
+
};
|
|
97
|
+
export const grape = {
|
|
98
|
+
id: 42,
|
|
99
|
+
key: 'grape',
|
|
100
|
+
displayName: 'Grape',
|
|
101
|
+
name: 'Grape',
|
|
102
|
+
hexValue: '#884dff',
|
|
103
|
+
value: '#884dff',
|
|
104
|
+
};
|
|
105
|
+
export const violet = {
|
|
106
|
+
id: 43,
|
|
107
|
+
key: 'violet',
|
|
108
|
+
displayName: 'Violet',
|
|
109
|
+
name: 'Violet',
|
|
110
|
+
hexValue: '#af38eb',
|
|
111
|
+
value: '#af38eb',
|
|
112
|
+
};
|
|
113
|
+
export const lavender = {
|
|
114
|
+
id: 44,
|
|
115
|
+
key: 'lavender',
|
|
116
|
+
displayName: 'Lavender',
|
|
117
|
+
name: 'Lavender',
|
|
118
|
+
hexValue: '#eb96eb',
|
|
119
|
+
value: '#eb96eb',
|
|
120
|
+
};
|
|
121
|
+
export const magenta = {
|
|
122
|
+
id: 45,
|
|
123
|
+
key: 'magenta',
|
|
124
|
+
displayName: 'Magenta',
|
|
125
|
+
name: 'Magenta',
|
|
126
|
+
hexValue: '#e05194',
|
|
127
|
+
value: '#e05194',
|
|
128
|
+
};
|
|
129
|
+
export const salmon = {
|
|
130
|
+
id: 46,
|
|
131
|
+
key: 'salmon',
|
|
132
|
+
displayName: 'Salmon',
|
|
133
|
+
name: 'Salmon',
|
|
134
|
+
hexValue: '#ff8d85',
|
|
135
|
+
value: '#ff8d85',
|
|
136
|
+
};
|
|
137
|
+
export const charcoal = {
|
|
138
|
+
id: 47,
|
|
139
|
+
key: 'charcoal',
|
|
140
|
+
displayName: 'Charcoal',
|
|
141
|
+
name: 'Charcoal',
|
|
142
|
+
hexValue: '#808080',
|
|
143
|
+
value: '#808080',
|
|
144
|
+
};
|
|
145
|
+
export const gray = {
|
|
146
|
+
id: 48,
|
|
147
|
+
key: 'gray',
|
|
148
|
+
displayName: 'Gray',
|
|
149
|
+
name: 'Gray',
|
|
150
|
+
hexValue: '#b8b8b8',
|
|
151
|
+
value: '#b8b8b8',
|
|
152
|
+
};
|
|
153
|
+
export const taupe = {
|
|
154
|
+
id: 49,
|
|
155
|
+
key: 'taupe',
|
|
156
|
+
displayName: 'Taupe',
|
|
157
|
+
name: 'Taupe',
|
|
158
|
+
hexValue: '#ccac93',
|
|
159
|
+
value: '#ccac93',
|
|
160
|
+
};
|
|
161
|
+
export const colors = [
|
|
162
|
+
berryRed,
|
|
163
|
+
red,
|
|
164
|
+
orange,
|
|
165
|
+
yellow,
|
|
166
|
+
oliveGreen,
|
|
167
|
+
limeGreen,
|
|
168
|
+
green,
|
|
169
|
+
mintGreen,
|
|
170
|
+
turquoise,
|
|
171
|
+
skyBlue,
|
|
172
|
+
lightBlue,
|
|
173
|
+
blue,
|
|
174
|
+
grape,
|
|
175
|
+
violet,
|
|
176
|
+
lavender,
|
|
177
|
+
magenta,
|
|
178
|
+
salmon,
|
|
179
|
+
charcoal,
|
|
180
|
+
gray,
|
|
181
|
+
taupe,
|
|
182
|
+
];
|
|
183
|
+
export const defaultColor = charcoal;
|
|
184
|
+
/**
|
|
185
|
+
* @private
|
|
186
|
+
* @deprecated Use {@link getColorByKey} instead
|
|
187
|
+
*/
|
|
188
|
+
export function getColorById(colorId) {
|
|
189
|
+
const color = colors.find((color) => color.id === colorId);
|
|
190
|
+
return color !== null && color !== void 0 ? color : defaultColor;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* @private
|
|
194
|
+
* @deprecated Use {@link getColorByKey} instead
|
|
195
|
+
*/
|
|
196
|
+
export function getColorByName(colorName) {
|
|
197
|
+
const color = colors.find((color) => color.name === colorName);
|
|
198
|
+
return color !== null && color !== void 0 ? color : defaultColor;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Retrieves a {@link Color} object by its key identifier.
|
|
202
|
+
*
|
|
203
|
+
* @param colorKey - The unique key identifier of the color to find (e.g., 'berry_red', 'sky_blue')
|
|
204
|
+
* @returns The matching Color object if found, otherwise returns the default color (charcoal)
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* const color = getColorByKey('berry_red');
|
|
209
|
+
* console.log(color.hexValue); // '#b8255f'
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export function getColorByKey(colorKey) {
|
|
213
|
+
const color = colors.find((color) => color.key === colorKey);
|
|
214
|
+
return color !== null && color !== void 0 ? color : defaultColor;
|
|
215
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { isNetworkError } from '../types/http.js';
|
|
13
|
+
/**
|
|
14
|
+
* Default retry configuration matching the original axios-retry behavior
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
17
|
+
retries: 3,
|
|
18
|
+
retryCondition: isNetworkError,
|
|
19
|
+
retryDelay: (retryNumber) => {
|
|
20
|
+
// First retry: immediate (0ms delay)
|
|
21
|
+
// Subsequent retries: 500ms delay
|
|
22
|
+
return retryNumber === 1 ? 0 : 500;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Converts Headers object to a plain object
|
|
27
|
+
*/
|
|
28
|
+
function headersToObject(headers) {
|
|
29
|
+
const result = {};
|
|
30
|
+
headers.forEach((value, key) => {
|
|
31
|
+
result[key] = value;
|
|
32
|
+
});
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates an AbortSignal that times out after the specified duration
|
|
37
|
+
*/
|
|
38
|
+
function createTimeoutSignal(timeoutMs, existingSignal) {
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
// Timeout logic
|
|
41
|
+
const timeoutId = setTimeout(() => {
|
|
42
|
+
controller.abort(new Error(`Request timeout after ${timeoutMs}ms`));
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
// If there's an existing signal, forward its abort
|
|
45
|
+
if (existingSignal) {
|
|
46
|
+
if (existingSignal.aborted) {
|
|
47
|
+
clearTimeout(timeoutId);
|
|
48
|
+
controller.abort(existingSignal.reason);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
existingSignal.addEventListener('abort', () => {
|
|
52
|
+
clearTimeout(timeoutId);
|
|
53
|
+
controller.abort(existingSignal.reason);
|
|
54
|
+
}, { once: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Clean up timeout when request completes
|
|
58
|
+
controller.signal.addEventListener('abort', () => {
|
|
59
|
+
clearTimeout(timeoutId);
|
|
60
|
+
});
|
|
61
|
+
return controller.signal;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Performs a fetch request with retry logic and timeout support
|
|
65
|
+
*/
|
|
66
|
+
export async function fetchWithRetry(args) {
|
|
67
|
+
const { url, options = {}, retryConfig = {} } = args;
|
|
68
|
+
const config = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), retryConfig);
|
|
69
|
+
const { timeout, signal: userSignal } = options, fetchOptions = __rest(options, ["timeout", "signal"]);
|
|
70
|
+
let lastError;
|
|
71
|
+
for (let attempt = 0; attempt <= config.retries; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
// Set up timeout and signal handling
|
|
74
|
+
let requestSignal = userSignal || undefined;
|
|
75
|
+
if (timeout && timeout > 0) {
|
|
76
|
+
requestSignal = createTimeoutSignal(timeout, requestSignal);
|
|
77
|
+
}
|
|
78
|
+
const response = await fetch(url, Object.assign(Object.assign({}, fetchOptions), { signal: requestSignal }));
|
|
79
|
+
// Check if the response is successful
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
82
|
+
const error = new Error(errorMessage);
|
|
83
|
+
error.status = response.status;
|
|
84
|
+
error.statusText = response.statusText;
|
|
85
|
+
error.response = {
|
|
86
|
+
data: undefined, // Will be set below if we can parse the response
|
|
87
|
+
status: response.status,
|
|
88
|
+
statusText: response.statusText,
|
|
89
|
+
headers: headersToObject(response.headers),
|
|
90
|
+
};
|
|
91
|
+
// Try to get response body for error details
|
|
92
|
+
try {
|
|
93
|
+
const responseText = await response.text();
|
|
94
|
+
let responseData;
|
|
95
|
+
try {
|
|
96
|
+
responseData = responseText ? JSON.parse(responseText) : undefined;
|
|
97
|
+
}
|
|
98
|
+
catch (_a) {
|
|
99
|
+
responseData = responseText;
|
|
100
|
+
}
|
|
101
|
+
error.data = responseData;
|
|
102
|
+
error.response.data = responseData;
|
|
103
|
+
}
|
|
104
|
+
catch (_b) {
|
|
105
|
+
// If we can't read the response body, that's OK
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
// Parse response
|
|
110
|
+
const responseText = await response.text();
|
|
111
|
+
let data;
|
|
112
|
+
try {
|
|
113
|
+
data = responseText ? JSON.parse(responseText) : undefined;
|
|
114
|
+
}
|
|
115
|
+
catch (_c) {
|
|
116
|
+
// If JSON parsing fails, return the raw text
|
|
117
|
+
data = responseText;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
data,
|
|
121
|
+
status: response.status,
|
|
122
|
+
statusText: response.statusText,
|
|
123
|
+
headers: headersToObject(response.headers),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
lastError = error;
|
|
128
|
+
// Check if this error should trigger a retry
|
|
129
|
+
const shouldRetry = attempt < config.retries && config.retryCondition(lastError);
|
|
130
|
+
if (!shouldRetry) {
|
|
131
|
+
// Add network error flag for network errors
|
|
132
|
+
if (isNetworkError(lastError)) {
|
|
133
|
+
const networkError = lastError;
|
|
134
|
+
networkError.isNetworkError = true;
|
|
135
|
+
}
|
|
136
|
+
throw lastError;
|
|
137
|
+
}
|
|
138
|
+
// Wait before retrying
|
|
139
|
+
const delay = config.retryDelay(attempt + 1);
|
|
140
|
+
if (delay > 0) {
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// This should never be reached, but just in case
|
|
146
|
+
throw lastError || new Error('Request failed after retries');
|
|
147
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import FormData from 'form-data';
|
|
2
|
+
import { createReadStream } from 'fs';
|
|
3
|
+
import { basename } from 'path';
|
|
4
|
+
import { fetchWithRetry } from './fetch-with-retry.js';
|
|
5
|
+
/**
|
|
6
|
+
* Helper function to determine content-type from filename extension.
|
|
7
|
+
* @param fileName - The filename to analyze
|
|
8
|
+
* @returns The appropriate MIME type
|
|
9
|
+
*/
|
|
10
|
+
function getContentTypeFromFileName(fileName) {
|
|
11
|
+
const extension = fileName.toLowerCase().split('.').pop();
|
|
12
|
+
switch (extension) {
|
|
13
|
+
case 'png':
|
|
14
|
+
return 'image/png';
|
|
15
|
+
case 'jpg':
|
|
16
|
+
case 'jpeg':
|
|
17
|
+
return 'image/jpeg';
|
|
18
|
+
case 'gif':
|
|
19
|
+
return 'image/gif';
|
|
20
|
+
case 'webp':
|
|
21
|
+
return 'image/webp';
|
|
22
|
+
case 'svg':
|
|
23
|
+
return 'image/svg+xml';
|
|
24
|
+
default:
|
|
25
|
+
return 'application/octet-stream';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Uploads a file using multipart/form-data.
|
|
30
|
+
*
|
|
31
|
+
* This is a shared utility for uploading files to Todoist endpoints that require
|
|
32
|
+
* multipart/form-data content type (e.g., file uploads, workspace logo uploads).
|
|
33
|
+
*
|
|
34
|
+
* @param baseUrl - The base API URL (e.g., https://api.todoist.com/api/v1/)
|
|
35
|
+
* @param authToken - The authentication token
|
|
36
|
+
* @param endpoint - The relative endpoint path (e.g., 'uploads', 'workspaces/logo')
|
|
37
|
+
* @param file - The file content (Buffer, ReadableStream, or file system path)
|
|
38
|
+
* @param fileName - Optional file name (required for Buffer/Stream, optional for paths)
|
|
39
|
+
* @param additionalFields - Additional form fields to include (e.g., project_id, workspace_id)
|
|
40
|
+
* @param requestId - Optional request ID for idempotency
|
|
41
|
+
* @returns The response data from the server
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // Upload from a file path
|
|
46
|
+
* const result = await uploadMultipartFile(
|
|
47
|
+
* 'https://api.todoist.com/api/v1/',
|
|
48
|
+
* 'my-token',
|
|
49
|
+
* 'uploads',
|
|
50
|
+
* '/path/to/file.pdf',
|
|
51
|
+
* undefined,
|
|
52
|
+
* { project_id: '12345' }
|
|
53
|
+
* )
|
|
54
|
+
*
|
|
55
|
+
* // Upload from a Buffer
|
|
56
|
+
* const buffer = Buffer.from('file content')
|
|
57
|
+
* const result = await uploadMultipartFile(
|
|
58
|
+
* 'https://api.todoist.com/api/v1/',
|
|
59
|
+
* 'my-token',
|
|
60
|
+
* 'uploads',
|
|
61
|
+
* buffer,
|
|
62
|
+
* 'document.pdf',
|
|
63
|
+
* { project_id: '12345' }
|
|
64
|
+
* )
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export async function uploadMultipartFile(args) {
|
|
68
|
+
const { baseUrl, authToken, endpoint, file, fileName, additionalFields, requestId } = args;
|
|
69
|
+
const form = new FormData();
|
|
70
|
+
// Determine file type and add to form data
|
|
71
|
+
if (typeof file === 'string') {
|
|
72
|
+
// File path - create read stream
|
|
73
|
+
const filePath = file;
|
|
74
|
+
const resolvedFileName = fileName || basename(filePath);
|
|
75
|
+
form.append('file', createReadStream(filePath), resolvedFileName);
|
|
76
|
+
}
|
|
77
|
+
else if (Buffer.isBuffer(file)) {
|
|
78
|
+
// Buffer - require fileName
|
|
79
|
+
if (!fileName) {
|
|
80
|
+
throw new Error('fileName is required when uploading from a Buffer');
|
|
81
|
+
}
|
|
82
|
+
// Detect content-type from filename extension
|
|
83
|
+
const contentType = getContentTypeFromFileName(fileName);
|
|
84
|
+
form.append('file', file, {
|
|
85
|
+
filename: fileName,
|
|
86
|
+
contentType: contentType,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Stream - require fileName
|
|
91
|
+
if (!fileName) {
|
|
92
|
+
throw new Error('fileName is required when uploading from a stream');
|
|
93
|
+
}
|
|
94
|
+
form.append('file', file, fileName);
|
|
95
|
+
}
|
|
96
|
+
// Add additional fields to the form
|
|
97
|
+
for (const [key, value] of Object.entries(additionalFields)) {
|
|
98
|
+
if (value !== undefined && value !== null) {
|
|
99
|
+
form.append(key, value.toString());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Build the full URL
|
|
103
|
+
const url = `${baseUrl}${endpoint}`;
|
|
104
|
+
// Prepare headers
|
|
105
|
+
const headers = Object.assign({ Authorization: `Bearer ${authToken}` }, form.getHeaders());
|
|
106
|
+
if (requestId) {
|
|
107
|
+
headers['X-Request-Id'] = requestId;
|
|
108
|
+
}
|
|
109
|
+
// Make the request using fetch
|
|
110
|
+
const response = await fetchWithRetry({
|
|
111
|
+
url,
|
|
112
|
+
options: {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: form, // FormData from 'form-data' package is compatible with fetch
|
|
115
|
+
headers,
|
|
116
|
+
timeout: 30000, // 30 second timeout for file uploads
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
return response.data;
|
|
120
|
+
}
|
|
@@ -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,112 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TODOIST_WEB_URI } from '../consts/endpoints.js';
|
|
2
|
+
/**
|
|
3
|
+
* Formats a Date object to YYYY-MM-DD string format.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
* @param date The Date object to format.
|
|
7
|
+
* @returns The formatted date string in YYYY-MM-DD format.
|
|
8
|
+
*/
|
|
9
|
+
export function formatDateToYYYYMMDD(date) {
|
|
10
|
+
const year = date.getFullYear();
|
|
11
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
12
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
13
|
+
return `${year}-${month}-${day}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generate the URL for a given task.
|
|
17
|
+
*
|
|
18
|
+
* @param taskId The ID of the task.
|
|
19
|
+
* @param content The content of the task.
|
|
20
|
+
* @returns The URL string for the task view.
|
|
21
|
+
*/
|
|
22
|
+
export function getTaskUrl(taskId, content) {
|
|
23
|
+
const slug = content ? slugify(content) : undefined;
|
|
24
|
+
const path = slug ? `${slug}-${taskId}` : taskId;
|
|
25
|
+
return `${TODOIST_WEB_URI}/task/${path}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate the URL for a given project.
|
|
29
|
+
*
|
|
30
|
+
* @param projectId The ID of the project.
|
|
31
|
+
* @param name The name of the project.
|
|
32
|
+
* @returns The URL string for the project view.
|
|
33
|
+
*/
|
|
34
|
+
export function getProjectUrl(projectId, name) {
|
|
35
|
+
const slug = name ? slugify(name) : undefined;
|
|
36
|
+
const path = slug ? `${slug}-${projectId}` : projectId;
|
|
37
|
+
return `${TODOIST_WEB_URI}/project/${path}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Generate the URL for a given section.
|
|
41
|
+
*
|
|
42
|
+
* @param sectionId The ID of the section.
|
|
43
|
+
* @param name The name of the section.
|
|
44
|
+
* @returns The URL string for the section view.
|
|
45
|
+
*/
|
|
46
|
+
export function getSectionUrl(sectionId, name) {
|
|
47
|
+
const slug = name ? slugify(name) : undefined;
|
|
48
|
+
const path = slug ? `${slug}-${sectionId}` : sectionId;
|
|
49
|
+
return `${TODOIST_WEB_URI}/section/${path}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Slugify function borrowed from Django.
|
|
53
|
+
*
|
|
54
|
+
* @param value The string to slugify.
|
|
55
|
+
* @returns The slugified string.
|
|
56
|
+
*/
|
|
57
|
+
function slugify(value) {
|
|
58
|
+
// Convert to ASCII
|
|
59
|
+
let result = value.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
|
|
60
|
+
// Remove non-ASCII characters
|
|
61
|
+
result = result.replace(/[^\x20-\x7E]/g, '');
|
|
62
|
+
// Convert to lowercase and replace non-alphanumeric characters with dashes
|
|
63
|
+
result = result.toLowerCase().replace(/[^\w\s-]/g, '');
|
|
64
|
+
// Replace spaces and repeated dashes with single dashes
|
|
65
|
+
result = result.replace(/[-\s]+/g, '-');
|
|
66
|
+
// Strip dashes from the beginning and end
|
|
67
|
+
return result.replace(/^-+|-+$/g, '');
|
|
68
|
+
}
|