@doist/todoist-sdk 14.0.2 → 14.2.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 +46 -1
- package/dist/cjs/clients/view-options-client.js +25 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/todoist-api.js +30 -0
- package/dist/cjs/transport/http-dispatcher.js +25 -0
- package/dist/cjs/types/sync/resources/view-options.js +13 -4
- package/dist/cjs/utils/index.js +2 -0
- package/dist/cjs/utils/multipart-upload.js +171 -57
- package/dist/cjs/utils/task-sorting.js +281 -0
- package/dist/cjs/utils/view-options.js +19 -0
- package/dist/esm/clients/view-options-client.js +21 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/todoist-api.js +30 -0
- package/dist/esm/transport/http-dispatcher.js +25 -0
- package/dist/esm/types/sync/resources/view-options.js +12 -3
- package/dist/esm/utils/index.js +2 -0
- package/dist/esm/utils/multipart-upload.js +171 -57
- package/dist/esm/utils/task-sorting.js +277 -0
- package/dist/esm/utils/view-options.js +15 -0
- package/dist/types/clients/view-options-client.d.ts +8 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/todoist-api.d.ts +24 -1
- package/dist/types/transport/http-dispatcher.d.ts +26 -1
- package/dist/types/types/sync/commands/project-view-options.d.ts +1 -4
- package/dist/types/types/sync/commands/view-options.d.ts +2 -1
- package/dist/types/types/sync/resources/view-options.d.ts +37 -7
- package/dist/types/types/sync/response.d.ts +24 -5
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/task-sorting.d.ts +38 -0
- package/dist/types/utils/validators.d.ts +24 -6
- package/dist/types/utils/view-options.d.ts +11 -0
- package/package.json +2 -3
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
|
|
2
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
3
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
4
|
+
var m = o[Symbol.asyncIterator], i;
|
|
5
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
6
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
7
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
8
|
+
};
|
|
9
|
+
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
|
|
10
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
11
|
+
var g = generator.apply(thisArg, _arguments || []), i, q = [];
|
|
12
|
+
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
|
|
13
|
+
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
|
|
14
|
+
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
|
|
15
|
+
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
|
|
16
|
+
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
|
|
17
|
+
function fulfill(value) { resume("next", value); }
|
|
18
|
+
function reject(value) { resume("throw", value); }
|
|
19
|
+
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
|
|
20
|
+
};
|
|
21
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
1
22
|
import { fetchWithRetry } from '../transport/fetch-with-retry.js';
|
|
2
23
|
import { camelCaseKeys } from './case-conversion.js';
|
|
3
24
|
/**
|
|
@@ -23,6 +44,116 @@ function getContentTypeFromFileName(fileName) {
|
|
|
23
44
|
return 'application/octet-stream';
|
|
24
45
|
}
|
|
25
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Escapes a value for use inside a quoted `Content-Disposition` parameter,
|
|
49
|
+
* where a raw quote would end the value early and a raw CR or LF would end the
|
|
50
|
+
* header. Note that a lone CR needs escaping too, not just CRLF pairs.
|
|
51
|
+
*
|
|
52
|
+
* This is the same escaping the platform's own `FormData` encoder applies, so
|
|
53
|
+
* servers see exactly what they would from any browser or from undici.
|
|
54
|
+
*/
|
|
55
|
+
function escapeDispositionValue(value) {
|
|
56
|
+
return value.replace(/"/g, '%22').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Lays out a `multipart/form-data` payload around a single file part, as the
|
|
60
|
+
* text that goes before and after the file content. Keeping the file content
|
|
61
|
+
* out of it lets the same layout serve both a `Blob` body and a streamed one.
|
|
62
|
+
*/
|
|
63
|
+
function layOutMultipart(args) {
|
|
64
|
+
const { fileName, fileContentType, additionalFields } = args;
|
|
65
|
+
const boundary = `----todoist-sdk-${uuidv4()}`;
|
|
66
|
+
const head = `--${boundary}\r\n` +
|
|
67
|
+
`Content-Disposition: form-data; name="file"; filename="${escapeDispositionValue(fileName)}"\r\n` +
|
|
68
|
+
`Content-Type: ${fileContentType}\r\n\r\n`;
|
|
69
|
+
let tail = '\r\n';
|
|
70
|
+
for (const [key, value] of Object.entries(additionalFields)) {
|
|
71
|
+
if (value !== undefined && value !== null) {
|
|
72
|
+
tail +=
|
|
73
|
+
`--${boundary}\r\n` +
|
|
74
|
+
`Content-Disposition: form-data; name="${escapeDispositionValue(key)}"\r\n\r\n` +
|
|
75
|
+
`${value.toString()}\r\n`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
tail += `--${boundary}--\r\n`;
|
|
79
|
+
return { boundary, head, tail };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Wraps a Node readable stream as a `multipart/form-data` request body.
|
|
83
|
+
*
|
|
84
|
+
* The result is a web `ReadableStream`, which both the runtime's `fetch` and
|
|
85
|
+
* undici's own accept — unlike a Node stream or a `FormData`, either of which
|
|
86
|
+
* only works with one particular client. The file is never buffered, so the
|
|
87
|
+
* request is sent with `Transfer-Encoding: chunked`.
|
|
88
|
+
*/
|
|
89
|
+
function streamMultipartBody(file, parts) {
|
|
90
|
+
const encoder = new TextEncoder();
|
|
91
|
+
function chunks() {
|
|
92
|
+
return __asyncGenerator(this, arguments, function* chunks_1() {
|
|
93
|
+
var _a, e_1, _b, _c;
|
|
94
|
+
yield yield __await(encoder.encode(parts.head));
|
|
95
|
+
try {
|
|
96
|
+
for (var _d = true, file_1 = __asyncValues(file), file_1_1; file_1_1 = yield __await(file_1.next()), _a = file_1_1.done, !_a; _d = true) {
|
|
97
|
+
_c = file_1_1.value;
|
|
98
|
+
_d = false;
|
|
99
|
+
const chunk = _c;
|
|
100
|
+
yield yield __await(typeof chunk === 'string' ? encoder.encode(chunk) : new Uint8Array(chunk));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
104
|
+
finally {
|
|
105
|
+
try {
|
|
106
|
+
if (!_d && !_a && (_b = file_1.return)) yield __await(_b.call(file_1));
|
|
107
|
+
}
|
|
108
|
+
finally { if (e_1) throw e_1.error; }
|
|
109
|
+
}
|
|
110
|
+
yield yield __await(encoder.encode(parts.tail));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const iterator = chunks();
|
|
114
|
+
// Driven by hand rather than with `ReadableStream.from`, which is absent
|
|
115
|
+
// from this project's DOM lib types and is still missing in some browsers,
|
|
116
|
+
// or `Readable.toWeb`, which would drag a Node-only import into a module
|
|
117
|
+
// that browsers also load.
|
|
118
|
+
return new ReadableStream({
|
|
119
|
+
async pull(controller) {
|
|
120
|
+
const { value, done } = await iterator.next();
|
|
121
|
+
if (done) {
|
|
122
|
+
controller.close();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
controller.enqueue(value);
|
|
126
|
+
},
|
|
127
|
+
async cancel(reason) {
|
|
128
|
+
var _a;
|
|
129
|
+
await ((_a = iterator.return) === null || _a === void 0 ? void 0 : _a.call(iterator, reason));
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Builds the request body for a multipart upload.
|
|
135
|
+
*
|
|
136
|
+
* Deliberately avoids `FormData`. A `FormData` body is only encoded by the
|
|
137
|
+
* `fetch` that owns that `FormData` class — undici brands its own — so a body
|
|
138
|
+
* built from the global `FormData` and sent through undici's `fetch`, or the
|
|
139
|
+
* reverse, is stringified to the literal `"[object FormData]"` and the upload
|
|
140
|
+
* silently carries no file. The `form-data` package fares worse: neither
|
|
141
|
+
* `fetch` accepts it as a body at all. `Blob` and `ReadableStream` carry no
|
|
142
|
+
* such brand and work with whichever `fetch` dispatches the request,
|
|
143
|
+
* including a caller-supplied `customFetch`.
|
|
144
|
+
*/
|
|
145
|
+
function buildMultipartBody(args) {
|
|
146
|
+
const { file, fileName, fileContentType, additionalFields } = args;
|
|
147
|
+
const parts = layOutMultipart({ fileName, fileContentType, additionalFields });
|
|
148
|
+
const contentType = `multipart/form-data; boundary=${parts.boundary}`;
|
|
149
|
+
if (file instanceof Blob) {
|
|
150
|
+
// A Blob keeps its length, so the request gets a Content-Length and
|
|
151
|
+
// stays replayable if it has to be retried. File-backed Blobs (from
|
|
152
|
+
// `fs.openAsBlob`) stay lazy here, so nothing is read into memory.
|
|
153
|
+
return { body: new Blob([parts.head, file, parts.tail]), contentType, isStreamed: false };
|
|
154
|
+
}
|
|
155
|
+
return { body: streamMultipartBody(file, parts), contentType, isStreamed: true };
|
|
156
|
+
}
|
|
26
157
|
/**
|
|
27
158
|
* Uploads a file using multipart/form-data.
|
|
28
159
|
*
|
|
@@ -75,69 +206,52 @@ export async function uploadMultipartFile(args) {
|
|
|
75
206
|
if (requestId) {
|
|
76
207
|
headers['X-Request-Id'] = requestId;
|
|
77
208
|
}
|
|
209
|
+
// Resolve the file to something every `fetch` accepts as a body part: a
|
|
210
|
+
// Blob where the size is known up front, or a Node stream that gets piped
|
|
211
|
+
// through untouched.
|
|
212
|
+
let filePart;
|
|
213
|
+
let resolvedFileName;
|
|
78
214
|
if (file instanceof Blob) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
215
|
+
filePart = file;
|
|
216
|
+
resolvedFileName = fileName || (file instanceof File ? file.name : undefined) || 'upload';
|
|
217
|
+
}
|
|
218
|
+
else if (typeof file === 'string') {
|
|
219
|
+
// File path: Node-only, so import lazily to keep browser bundles clean.
|
|
220
|
+
const [fsModule, pathModule] = await Promise.all([import('fs'), import('path')]);
|
|
221
|
+
resolvedFileName = fileName || pathModule.basename(file);
|
|
222
|
+
// `openAsBlob` reads the file lazily, so a large upload is not pulled
|
|
223
|
+
// into memory, and the resulting body still has a known length.
|
|
224
|
+
filePart = await fsModule.openAsBlob(file);
|
|
225
|
+
}
|
|
226
|
+
else if (Buffer.isBuffer(file)) {
|
|
227
|
+
if (!fileName) {
|
|
228
|
+
throw new Error('fileName is required when uploading from a Buffer');
|
|
87
229
|
}
|
|
88
|
-
|
|
89
|
-
|
|
230
|
+
resolvedFileName = fileName;
|
|
231
|
+
filePart = new Blob([
|
|
232
|
+
new Uint8Array(file.buffer, file.byteOffset, file.byteLength),
|
|
233
|
+
]);
|
|
90
234
|
}
|
|
91
235
|
else {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
import('form-data'),
|
|
95
|
-
import('fs'),
|
|
96
|
-
import('path'),
|
|
97
|
-
]);
|
|
98
|
-
const FormData = FormDataModule.default;
|
|
99
|
-
const form = new FormData();
|
|
100
|
-
if (typeof file === 'string') {
|
|
101
|
-
// File path - create read stream
|
|
102
|
-
const resolvedFileName = fileName || pathModule.basename(file);
|
|
103
|
-
form.append('file', fsModule.createReadStream(file), resolvedFileName);
|
|
104
|
-
}
|
|
105
|
-
else if (Buffer.isBuffer(file)) {
|
|
106
|
-
// Buffer - require fileName
|
|
107
|
-
if (!fileName) {
|
|
108
|
-
throw new Error('fileName is required when uploading from a Buffer');
|
|
109
|
-
}
|
|
110
|
-
const contentType = getContentTypeFromFileName(fileName);
|
|
111
|
-
form.append('file', file, {
|
|
112
|
-
filename: fileName,
|
|
113
|
-
contentType: contentType,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
// Stream - require fileName
|
|
118
|
-
if (!fileName) {
|
|
119
|
-
throw new Error('fileName is required when uploading from a stream');
|
|
120
|
-
}
|
|
121
|
-
form.append('file', file, fileName);
|
|
236
|
+
if (!fileName) {
|
|
237
|
+
throw new Error('fileName is required when uploading from a stream');
|
|
122
238
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
form.append(key, value.toString());
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
Object.assign(headers, form.getHeaders());
|
|
129
|
-
body = form;
|
|
239
|
+
resolvedFileName = fileName;
|
|
240
|
+
filePart = file;
|
|
130
241
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
headers,
|
|
138
|
-
timeout: 30000, // 30 second timeout for file uploads
|
|
139
|
-
},
|
|
140
|
-
customFetch,
|
|
242
|
+
const multipart = buildMultipartBody({
|
|
243
|
+
file: filePart,
|
|
244
|
+
fileName: resolvedFileName,
|
|
245
|
+
fileContentType: (filePart instanceof Blob ? filePart.type : undefined) ||
|
|
246
|
+
getContentTypeFromFileName(resolvedFileName),
|
|
247
|
+
additionalFields,
|
|
141
248
|
});
|
|
249
|
+
// The boundary is ours, so Content-Type has to be set explicitly — `fetch`
|
|
250
|
+
// only fills it in for a `FormData` body.
|
|
251
|
+
headers['Content-Type'] = multipart.contentType;
|
|
252
|
+
body = multipart.body;
|
|
253
|
+
// Make the request using fetch
|
|
254
|
+
const response = await fetchWithRetry(Object.assign(Object.assign({ url, options: Object.assign({ method: 'POST', body,
|
|
255
|
+
headers, timeout: 30000 }, (multipart.isStreamed ? { duplex: 'half' } : {})) }, (multipart.isStreamed ? { retryConfig: { retries: 0 } } : {})), { customFetch }));
|
|
142
256
|
return camelCaseKeys(response.data);
|
|
143
257
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/** Todoist's default task-ordering hierarchies. */
|
|
2
|
+
export const DEFAULT_TASK_ORDERS = ['PRIORITY_FIRST', 'DATE_FIRST'];
|
|
3
|
+
const DEFAULT_TIMEZONE = 'UTC';
|
|
4
|
+
const DEFAULT_LOCALE = 'en';
|
|
5
|
+
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
6
|
+
const DATE_TIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?$/;
|
|
7
|
+
const EXPLICIT_TIMEZONE_PATTERN = /(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
8
|
+
const DATE_TIME_FORMATTERS = new Map();
|
|
9
|
+
function compareNumber(a, b) {
|
|
10
|
+
if (a === b)
|
|
11
|
+
return 0;
|
|
12
|
+
return a < b ? -1 : 1;
|
|
13
|
+
}
|
|
14
|
+
function isValidDateParts(year, month, day) {
|
|
15
|
+
const date = new Date(0);
|
|
16
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
17
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
18
|
+
return (date.getUTCFullYear() === year &&
|
|
19
|
+
date.getUTCMonth() === month - 1 &&
|
|
20
|
+
date.getUTCDate() === day);
|
|
21
|
+
}
|
|
22
|
+
function dateSortValue({ year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0, isAllDay = false, }) {
|
|
23
|
+
if (!isValidDateParts(year, month, day) ||
|
|
24
|
+
hour < 0 ||
|
|
25
|
+
hour > 23 ||
|
|
26
|
+
minute < 0 ||
|
|
27
|
+
minute > 59 ||
|
|
28
|
+
second < 0 ||
|
|
29
|
+
second > 59 ||
|
|
30
|
+
millisecond < 0 ||
|
|
31
|
+
millisecond > 999) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
day: year * 10000 + month * 100 + day,
|
|
36
|
+
kind: isAllDay ? 0 : 1,
|
|
37
|
+
time: ((hour * 60 + minute) * 60 + second) * 1000 + millisecond,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function parseDateOnly(value) {
|
|
41
|
+
const match = DATE_ONLY_PATTERN.exec(value);
|
|
42
|
+
if (!match)
|
|
43
|
+
return null;
|
|
44
|
+
return dateSortValue({
|
|
45
|
+
year: Number(match[1]),
|
|
46
|
+
month: Number(match[2]),
|
|
47
|
+
day: Number(match[3]),
|
|
48
|
+
isAllDay: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function parseDateTimeComponents(value) {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
const match = DATE_TIME_PATTERN.exec(value);
|
|
54
|
+
if (!match)
|
|
55
|
+
return null;
|
|
56
|
+
const milliseconds = Number(((_a = match[7]) !== null && _a !== void 0 ? _a : '').padEnd(3, '0').slice(0, 3));
|
|
57
|
+
const components = {
|
|
58
|
+
year: Number(match[1]),
|
|
59
|
+
month: Number(match[2]),
|
|
60
|
+
day: Number(match[3]),
|
|
61
|
+
hour: Number(match[4]),
|
|
62
|
+
minute: Number(match[5]),
|
|
63
|
+
second: Number((_b = match[6]) !== null && _b !== void 0 ? _b : 0),
|
|
64
|
+
millisecond: milliseconds,
|
|
65
|
+
};
|
|
66
|
+
return dateSortValue(components) === null ? null : components;
|
|
67
|
+
}
|
|
68
|
+
function createDateTimeFormatter(timezone, fallback) {
|
|
69
|
+
const cached = DATE_TIME_FORMATTERS.get(timezone);
|
|
70
|
+
if (cached)
|
|
71
|
+
return cached;
|
|
72
|
+
try {
|
|
73
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
74
|
+
timeZone: timezone,
|
|
75
|
+
calendar: 'iso8601',
|
|
76
|
+
numberingSystem: 'latn',
|
|
77
|
+
year: 'numeric',
|
|
78
|
+
month: '2-digit',
|
|
79
|
+
day: '2-digit',
|
|
80
|
+
hour: '2-digit',
|
|
81
|
+
minute: '2-digit',
|
|
82
|
+
second: '2-digit',
|
|
83
|
+
hourCycle: 'h23',
|
|
84
|
+
});
|
|
85
|
+
DATE_TIME_FORMATTERS.set(timezone, formatter);
|
|
86
|
+
return formatter;
|
|
87
|
+
}
|
|
88
|
+
catch (_a) {
|
|
89
|
+
return fallback !== null && fallback !== void 0 ? fallback : createDateTimeFormatter(DEFAULT_TIMEZONE);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function componentsAt(timestamp, formatter) {
|
|
93
|
+
const parts = formatter.formatToParts(new Date(timestamp));
|
|
94
|
+
function part(type) {
|
|
95
|
+
var _a;
|
|
96
|
+
return Number((_a = parts.find((entry) => entry.type === type)) === null || _a === void 0 ? void 0 : _a.value);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
year: part('year'),
|
|
100
|
+
month: part('month'),
|
|
101
|
+
day: part('day'),
|
|
102
|
+
hour: part('hour'),
|
|
103
|
+
minute: part('minute'),
|
|
104
|
+
second: part('second'),
|
|
105
|
+
millisecond: new Date(timestamp).getUTCMilliseconds(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function componentsAsUtcTimestamp({ year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0, }) {
|
|
109
|
+
const date = new Date(0);
|
|
110
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
111
|
+
date.setUTCHours(hour, minute, second, millisecond);
|
|
112
|
+
return date.getTime();
|
|
113
|
+
}
|
|
114
|
+
/** Resolves a zoned wall clock to an instant, including daylight-saving offsets. */
|
|
115
|
+
function timestampForWallClock(components, formatter) {
|
|
116
|
+
const target = componentsAsUtcTimestamp(components);
|
|
117
|
+
let timestamp = target;
|
|
118
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
119
|
+
const adjustment = target - componentsAsUtcTimestamp(componentsAt(timestamp, formatter));
|
|
120
|
+
if (adjustment === 0)
|
|
121
|
+
return timestamp;
|
|
122
|
+
timestamp += adjustment;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function dateSortValueAt(timestamp, formatter) {
|
|
127
|
+
const value = dateSortValue(componentsAt(timestamp, formatter));
|
|
128
|
+
return value ? Object.assign(Object.assign({}, value), { time: timestamp }) : null;
|
|
129
|
+
}
|
|
130
|
+
function parseFixedDateTime(value, formatter) {
|
|
131
|
+
if (parseDateTimeComponents(value.replace(EXPLICIT_TIMEZONE_PATTERN, '')) === null)
|
|
132
|
+
return null;
|
|
133
|
+
const timestamp = Date.parse(value);
|
|
134
|
+
return Number.isNaN(timestamp) ? null : dateSortValueAt(timestamp, formatter);
|
|
135
|
+
}
|
|
136
|
+
function parseZonedFloatingDateTime(value, sourceFormatter, accountFormatter) {
|
|
137
|
+
const components = parseDateTimeComponents(value);
|
|
138
|
+
if (!components)
|
|
139
|
+
return null;
|
|
140
|
+
const timestamp = timestampForWallClock(components, sourceFormatter);
|
|
141
|
+
return timestamp === null ? null : dateSortValueAt(timestamp, accountFormatter);
|
|
142
|
+
}
|
|
143
|
+
function parseTaskDate(value, formatter, timezone) {
|
|
144
|
+
var _a;
|
|
145
|
+
return ((_a = parseDateOnly(value)) !== null && _a !== void 0 ? _a : (EXPLICIT_TIMEZONE_PATTERN.test(value)
|
|
146
|
+
? parseFixedDateTime(value, formatter)
|
|
147
|
+
: parseZonedFloatingDateTime(value, timezone ? createDateTimeFormatter(timezone, formatter) : formatter, formatter)));
|
|
148
|
+
}
|
|
149
|
+
function compareNullable(a, b, compare) {
|
|
150
|
+
if (a === null || b === null) {
|
|
151
|
+
if (a === b)
|
|
152
|
+
return 0;
|
|
153
|
+
return a === null ? 1 : -1;
|
|
154
|
+
}
|
|
155
|
+
return compare(a, b);
|
|
156
|
+
}
|
|
157
|
+
function compareDateValues(left, right) {
|
|
158
|
+
return (compareNumber(left.day, right.day) ||
|
|
159
|
+
compareNumber(left.kind, right.kind) ||
|
|
160
|
+
compareNumber(left.time, right.time));
|
|
161
|
+
}
|
|
162
|
+
function compareDate(a, b) {
|
|
163
|
+
return compareNullable(a, b, compareDateValues);
|
|
164
|
+
}
|
|
165
|
+
function compareOptionalNumber(a, b) {
|
|
166
|
+
return compareNullable(a, b, compareNumber);
|
|
167
|
+
}
|
|
168
|
+
function compareOptionalText(a, b, collator) {
|
|
169
|
+
return compareNullable(a, b, (left, right) => collator.compare(left, right));
|
|
170
|
+
}
|
|
171
|
+
function prepareTasks(tasks, context, { formatter, resolveAssignee }) {
|
|
172
|
+
return tasks.map((task, index) => {
|
|
173
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
174
|
+
const due = task.due
|
|
175
|
+
? parseTaskDate((_a = task.due.datetime) !== null && _a !== void 0 ? _a : task.due.date, formatter, task.due.timezone)
|
|
176
|
+
: null;
|
|
177
|
+
const deadline = task.deadline ? parseTaskDate(task.deadline.date, formatter) : null;
|
|
178
|
+
return {
|
|
179
|
+
task,
|
|
180
|
+
index,
|
|
181
|
+
due,
|
|
182
|
+
deadline,
|
|
183
|
+
schedule: due !== null && due !== void 0 ? due : deadline,
|
|
184
|
+
added: task.addedAt && !Number.isNaN(task.addedAt.getTime())
|
|
185
|
+
? task.addedAt.getTime()
|
|
186
|
+
: null,
|
|
187
|
+
project: (_c = (_b = context.projectOrder) === null || _b === void 0 ? void 0 : _b.get(task.projectId)) !== null && _c !== void 0 ? _c : null,
|
|
188
|
+
workspace: (_e = (_d = context.workspaceOrder) === null || _d === void 0 ? void 0 : _d.get(task.projectId)) !== null && _e !== void 0 ? _e : null,
|
|
189
|
+
assignee: resolveAssignee ? ((_g = (_f = context.assigneeName) === null || _f === void 0 ? void 0 : _f.call(context, task)) !== null && _g !== void 0 ? _g : null) : null,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function comparePriorityFirst(a, b) {
|
|
194
|
+
const common = compareNumber(b.task.priority, a.task.priority) ||
|
|
195
|
+
compareDate(a.due, b.due) ||
|
|
196
|
+
compareDate(a.deadline, b.deadline);
|
|
197
|
+
if (common !== 0)
|
|
198
|
+
return common;
|
|
199
|
+
const project = compareOptionalNumber(a.project, b.project);
|
|
200
|
+
if (project !== 0)
|
|
201
|
+
return project;
|
|
202
|
+
if (a.task.projectId === b.task.projectId) {
|
|
203
|
+
return compareNumber(a.task.childOrder, b.task.childOrder);
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
function compareDateFirst(a, b) {
|
|
208
|
+
return (compareDate(a.schedule, b.schedule) ||
|
|
209
|
+
compareNumber(b.task.priority, a.task.priority) ||
|
|
210
|
+
compareDate(a.deadline, b.deadline) ||
|
|
211
|
+
compareNumber(a.task.dayOrder, b.task.dayOrder) ||
|
|
212
|
+
compareOptionalNumber(a.added, b.added));
|
|
213
|
+
}
|
|
214
|
+
function compareDefault(a, b, defaultOrder) {
|
|
215
|
+
return defaultOrder === 'DATE_FIRST' ? compareDateFirst(a, b) : comparePriorityFirst(a, b);
|
|
216
|
+
}
|
|
217
|
+
function comparePrimary({ sortedBy, a, b, collator, }) {
|
|
218
|
+
switch (sortedBy) {
|
|
219
|
+
case 'ALPHABETICALLY':
|
|
220
|
+
return collator.compare(a.task.content, b.task.content);
|
|
221
|
+
case 'ASSIGNEE':
|
|
222
|
+
return compareOptionalText(a.assignee, b.assignee, collator);
|
|
223
|
+
case 'DUE_DATE':
|
|
224
|
+
return compareDate(a.due, b.due);
|
|
225
|
+
case 'DEADLINE':
|
|
226
|
+
return compareDate(a.deadline, b.deadline);
|
|
227
|
+
case 'ADDED_DATE':
|
|
228
|
+
return compareOptionalNumber(a.added, b.added);
|
|
229
|
+
case 'PRIORITY':
|
|
230
|
+
return compareNumber(a.task.priority, b.task.priority);
|
|
231
|
+
case 'PROJECT':
|
|
232
|
+
return compareOptionalNumber(a.project, b.project);
|
|
233
|
+
case 'WORKSPACE':
|
|
234
|
+
return compareOptionalNumber(a.workspace, b.workspace);
|
|
235
|
+
case 'MANUAL':
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function defaultSortOrder(sortedBy) {
|
|
240
|
+
return sortedBy === 'PRIORITY' ? 'DESC' : 'ASC';
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Sorts the supplied tasks using Todoist view-ordering rules.
|
|
244
|
+
*
|
|
245
|
+
* The function performs no I/O and always returns a new array. Exact project,
|
|
246
|
+
* workspace, assignee, localized text, and due-time ordering requires the
|
|
247
|
+
* corresponding context. Callers must sort each independent view section or
|
|
248
|
+
* group separately and preserve any subtask structure they render.
|
|
249
|
+
*
|
|
250
|
+
* This function sorts only the tasks supplied to it. Fetch every page first
|
|
251
|
+
* when globally correct ordering is required for a paginated query.
|
|
252
|
+
*/
|
|
253
|
+
export function sortTasks(tasks, options, context = {}) {
|
|
254
|
+
var _a, _b, _c;
|
|
255
|
+
const locale = Array.isArray(context.locale)
|
|
256
|
+
? [...context.locale]
|
|
257
|
+
: ((_a = context.locale) !== null && _a !== void 0 ? _a : DEFAULT_LOCALE);
|
|
258
|
+
const collator = new Intl.Collator(locale, { sensitivity: 'base', numeric: true });
|
|
259
|
+
const formatter = createDateTimeFormatter((_b = context.timezone) !== null && _b !== void 0 ? _b : DEFAULT_TIMEZONE);
|
|
260
|
+
const sortedBy = options.sortedBy;
|
|
261
|
+
const prepared = prepareTasks(tasks, context, {
|
|
262
|
+
formatter,
|
|
263
|
+
resolveAssignee: sortedBy === 'ASSIGNEE',
|
|
264
|
+
});
|
|
265
|
+
const direction = sortedBy && sortedBy !== 'MANUAL' ? ((_c = options.sortOrder) !== null && _c !== void 0 ? _c : defaultSortOrder(sortedBy)) : null;
|
|
266
|
+
const directionMultiplier = direction === 'DESC' ? -1 : 1;
|
|
267
|
+
return prepared
|
|
268
|
+
.sort((a, b) => {
|
|
269
|
+
if (sortedBy && sortedBy !== 'MANUAL') {
|
|
270
|
+
const primary = comparePrimary({ sortedBy, a, b, collator }) * directionMultiplier;
|
|
271
|
+
if (primary !== 0)
|
|
272
|
+
return primary;
|
|
273
|
+
}
|
|
274
|
+
return compareDefault(a, b, options.defaultOrder) || compareNumber(a.index, b.index);
|
|
275
|
+
})
|
|
276
|
+
.map(({ task }) => task);
|
|
277
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Returns whether saved view options are active rather than a deletion tombstone. */
|
|
2
|
+
export function isActiveViewOption(options) {
|
|
3
|
+
return options.isDeleted !== true;
|
|
4
|
+
}
|
|
5
|
+
/** Finds the active saved options for one logical view. */
|
|
6
|
+
export function findViewOptions(options, args) {
|
|
7
|
+
var _a;
|
|
8
|
+
const targetObjectId = (_a = args.objectId) !== null && _a !== void 0 ? _a : null;
|
|
9
|
+
return options.find((entry) => {
|
|
10
|
+
var _a;
|
|
11
|
+
return isActiveViewOption(entry) &&
|
|
12
|
+
((_a = entry.objectId) !== null && _a !== void 0 ? _a : null) === targetObjectId &&
|
|
13
|
+
args.viewTypes.includes(entry.viewType);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ViewOptions, ViewOptionsDeleteArgs, ViewOptionsSetArgs } from '../types/sync/index.js';
|
|
2
|
+
import { BaseClient } from './base-client.js';
|
|
3
|
+
/** Internal sub-client for saved view-option reads and writes. */
|
|
4
|
+
export declare class ViewOptionsClient extends BaseClient {
|
|
5
|
+
getViewOptions(): Promise<ViewOptions[]>;
|
|
6
|
+
setViewOptions(args: ViewOptionsSetArgs, requestId?: string): Promise<void>;
|
|
7
|
+
deleteViewOptions(args: ViewOptionsDeleteArgs, requestId?: string): Promise<void>;
|
|
8
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export * from './todoist-api.js';
|
|
|
2
2
|
export * from './authentication.js';
|
|
3
3
|
export * from './types/index.js';
|
|
4
4
|
export * from './utils/index.js';
|
|
5
|
-
export { getDefaultDispatcher } from './transport/http-dispatcher.js';
|
|
5
|
+
export { getDefaultDispatcher, getDefaultTransport } from './transport/http-dispatcher.js';
|
|
6
|
+
export type { DefaultTransport } from './transport/http-dispatcher.js';
|
|
@@ -14,6 +14,7 @@ import { ProductivityStats } from './types/productivity/index.js';
|
|
|
14
14
|
import { PersonalProject, WorkspaceProject, AddProjectArgs, UpdateProjectArgs, GetProjectsArgs, SearchProjectsArgs, GetProjectCollaboratorsArgs, GetProjectsResponse, GetProjectCollaboratorsResponse, GetArchivedProjectsArgs, GetArchivedProjectsResponse, MoveProjectToWorkspaceArgs, MoveProjectToPersonalArgs, GetArchivedProjectsCountArgs, GetArchivedProjectsCountResponse, GetProjectPermissionsResponse, GetFullProjectArgs, GetFullProjectResponse, JoinProjectResponse } from './types/projects/index.js';
|
|
15
15
|
import { AddReminderArgs, UpdateReminderArgs, AddLocationReminderArgs, UpdateLocationReminderArgs, GetRemindersArgs, GetRemindersResponse, GetLocationRemindersArgs, GetLocationRemindersResponse } from './types/reminders/index.js';
|
|
16
16
|
import { Section, AddSectionArgs, UpdateSectionArgs, GetSectionsArgs, SearchSectionsArgs, GetSectionsResponse } from './types/sections/index.js';
|
|
17
|
+
import type { SyncRequest, SyncResponse, ViewOptions, ViewOptionsDeleteArgs, ViewOptionsSetArgs } from './types/sync/index.js';
|
|
17
18
|
import type { Folder } from './types/sync/resources/folders.js';
|
|
18
19
|
import { Task, AddTaskArgs, GetTasksArgs, GetTasksByFilterArgs, UpdateTaskArgs, QuickAddTaskArgs, type MoveTaskArgs, GetCompletedTasksByCompletionDateArgs, GetCompletedTasksByDueDateArgs, GetCompletedTasksResponse, SearchCompletedTasksArgs, GetTasksResponse, GetAllCompletedTasksArgs, GetAllCompletedTasksResponse } from './types/tasks/index.js';
|
|
19
20
|
import { ExportTemplateFileArgs, ExportTemplateUrlArgs, ExportTemplateUrlResponse, CreateProjectFromTemplateArgs, CreateProjectFromTemplateResponse, ImportTemplateIntoProjectArgs, ImportTemplateFromIdArgs, ImportTemplateResponse } from './types/templates/index.js';
|
|
@@ -21,7 +22,6 @@ import type { AddUiExtensionArgs, UiExtension, UpdateUiExtensionArgs, UploadUiEx
|
|
|
21
22
|
import { UploadFileArgs, DeleteUploadArgs } from './types/uploads/index.js';
|
|
22
23
|
import { CurrentUser } from './types/users/index.js';
|
|
23
24
|
import { WorkspaceInvitation, WorkspacePlanDetails, JoinWorkspaceResult, Workspace, AddWorkspaceArgs, UpdateWorkspaceArgs, GetWorkspaceMembersActivityArgs, GetWorkspaceMembersActivityResponse, GetWorkspaceUserTasksArgs, GetWorkspaceUserTasksResponse, InviteWorkspaceUsersArgs, InviteWorkspaceUsersResponse, UpdateWorkspaceUserArgs, RemoveWorkspaceUserArgs, GetWorkspaceInvitationsArgs, DeleteWorkspaceInvitationArgs, WorkspaceInvitationActionArgs, JoinWorkspaceArgs, WorkspaceLogoArgs, GetWorkspacePlanDetailsArgs, GetWorkspaceUsersArgs, GetWorkspaceUsersResponse, GetWorkspaceProjectsArgs, WorkspaceInvitationsResponse, AllWorkspaceInvitationsResponse, WorkspaceLogoResponse } from './types/workspaces/index.js';
|
|
24
|
-
import { type SyncResponse, type SyncRequest } from './types/sync/index.js';
|
|
25
25
|
/**
|
|
26
26
|
* A client for interacting with the Todoist API v1.
|
|
27
27
|
* This class provides methods to manage tasks, projects, sections, labels, and comments in Todoist.
|
|
@@ -79,6 +79,7 @@ export declare class TodoistApi {
|
|
|
79
79
|
private readonly billingClient;
|
|
80
80
|
private readonly appClient;
|
|
81
81
|
private readonly uiExtensionClient;
|
|
82
|
+
private readonly viewOptionsClient;
|
|
82
83
|
constructor(
|
|
83
84
|
/**
|
|
84
85
|
* Your Todoist API token.
|
|
@@ -113,6 +114,28 @@ export declare class TodoistApi {
|
|
|
113
114
|
* ```
|
|
114
115
|
*/
|
|
115
116
|
sync(syncRequest: SyncRequest, requestId?: string): Promise<SyncResponse>;
|
|
117
|
+
/**
|
|
118
|
+
* Retrieves the authenticated user's active saved view options.
|
|
119
|
+
*
|
|
120
|
+
* @returns A promise that resolves to the active saved view options.
|
|
121
|
+
*/
|
|
122
|
+
getViewOptions(): Promise<ViewOptions[]>;
|
|
123
|
+
/**
|
|
124
|
+
* Sets saved options for a view.
|
|
125
|
+
*
|
|
126
|
+
* @param args - The saved view options to set.
|
|
127
|
+
* @param requestId - Optional custom identifier for the request.
|
|
128
|
+
* @returns A promise that resolves when the view options are saved.
|
|
129
|
+
*/
|
|
130
|
+
setViewOptions(args: ViewOptionsSetArgs, requestId?: string): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Deletes the saved options for a view.
|
|
133
|
+
*
|
|
134
|
+
* @param args - The view whose saved options to delete.
|
|
135
|
+
* @param requestId - Optional custom identifier for the request.
|
|
136
|
+
* @returns A promise that resolves when the view options are deleted.
|
|
137
|
+
*/
|
|
138
|
+
deleteViewOptions(args: ViewOptionsDeleteArgs, requestId?: string): Promise<void>;
|
|
116
139
|
/**
|
|
117
140
|
* Retrieves information about the authenticated user.
|
|
118
141
|
*
|
|
@@ -7,7 +7,7 @@ type UndiciFetch = typeof import('undici').fetch;
|
|
|
7
7
|
* never observe a dispatcher paired with a mismatched `fetch` — see
|
|
8
8
|
* {@link getDefaultFetch} for why the pairing matters.
|
|
9
9
|
*/
|
|
10
|
-
type DefaultTransport = {
|
|
10
|
+
export type DefaultTransport = {
|
|
11
11
|
dispatcher: Dispatcher;
|
|
12
12
|
fetch: UndiciFetch | undefined;
|
|
13
13
|
};
|
|
@@ -15,8 +15,33 @@ type DefaultTransport = {
|
|
|
15
15
|
* The default dispatcher and its paired `fetch`, as a single value so the two
|
|
16
16
|
* are always read consistently. Resolves to `undefined` outside Node (browser/
|
|
17
17
|
* edge), where callers use the global `fetch` with no dispatcher.
|
|
18
|
+
*
|
|
19
|
+
* Use this when supplying a `customFetch` that should still honour the proxy
|
|
20
|
+
* environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) and decode
|
|
21
|
+
* compressed responses the way the SDK does by default.
|
|
22
|
+
*
|
|
23
|
+
* Both halves must be used together. `dispatcher` decompresses the response
|
|
24
|
+
* body itself, so a `fetch` from a different undici build will try to decode
|
|
25
|
+
* it a second time and the request fails mid-stream with `terminated`. A
|
|
26
|
+
* `fetch` of `undefined` means the runtime's global `fetch` is the correct
|
|
27
|
+
* partner for this dispatcher.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const transport = await getDefaultTransport()
|
|
32
|
+
* const fetchImpl = transport?.fetch ?? fetch
|
|
33
|
+
* const response = await fetchImpl(url, { ...options, dispatcher: transport?.dispatcher })
|
|
34
|
+
* ```
|
|
18
35
|
*/
|
|
19
36
|
export declare function getDefaultTransport(): Promise<DefaultTransport | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* The default dispatcher on its own.
|
|
39
|
+
*
|
|
40
|
+
* @deprecated Use {@link getDefaultTransport} instead, which returns this
|
|
41
|
+
* dispatcher together with the `fetch` it must be used with. Attaching this
|
|
42
|
+
* dispatcher to the runtime's global `fetch` decodes compressed responses
|
|
43
|
+
* twice on Node 26 and the request fails with `terminated`.
|
|
44
|
+
*/
|
|
20
45
|
export declare function getDefaultDispatcher(): Promise<Dispatcher | undefined>;
|
|
21
46
|
/**
|
|
22
47
|
* The `fetch` implementation that must be used with the default dispatcher.
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type { ViewMode, GroupedBy, SortedBy, SortOrder
|
|
2
|
-
export type CalendarSettings = {
|
|
3
|
-
layout?: CalendarLayout;
|
|
4
|
-
};
|
|
1
|
+
import type { CalendarSettings, ViewMode, GroupedBy, SortedBy, SortOrder } from '../resources/view-options.js';
|
|
5
2
|
export type ProjectViewOptionsDefaultsSetArgs = {
|
|
6
3
|
projectId: string;
|
|
7
4
|
viewMode?: ViewMode | null;
|