@doist/comms-sdk 0.3.0 → 0.4.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/dist/cjs/clients/attachments-client.js +71 -0
- package/dist/cjs/comms-api.js +2 -0
- package/dist/cjs/consts/endpoints.js +2 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/utils/multipart-upload.js +144 -0
- package/dist/esm/clients/attachments-client.js +67 -0
- package/dist/esm/comms-api.js +2 -0
- package/dist/esm/consts/endpoints.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/utils/multipart-upload.js +140 -0
- package/dist/types/clients/attachments-client.d.ts +39 -0
- package/dist/types/comms-api.d.ts +2 -0
- package/dist/types/consts/endpoints.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/types/requests.d.ts +22 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/multipart-upload.d.ts +58 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AttachmentsClient = void 0;
|
|
4
|
+
const endpoints_1 = require("../consts/endpoints");
|
|
5
|
+
const entities_1 = require("../types/entities");
|
|
6
|
+
const multipart_upload_1 = require("../utils/multipart-upload");
|
|
7
|
+
const uuidv7_1 = require("../utils/uuidv7");
|
|
8
|
+
const base_client_1 = require("./base-client");
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the `attachment_id`: validate a caller-supplied value (throwing an
|
|
11
|
+
* {@link UuidV7Error} that names `attachmentId` rather than the generic `id`) or
|
|
12
|
+
* mint a fresh one. Wraps {@link resolveCreateId} so the error contract matches the
|
|
13
|
+
* client's actual parameter.
|
|
14
|
+
*/
|
|
15
|
+
function resolveAttachmentId(attachmentId) {
|
|
16
|
+
if (attachmentId !== undefined && !(0, uuidv7_1.isValidUuidV7Base58)(attachmentId)) {
|
|
17
|
+
throw new uuidv7_1.UuidV7Error(`invalid attachmentId ${JSON.stringify(attachmentId)} — use generateId() or omit \`attachmentId\` and let the SDK mint one.`);
|
|
18
|
+
}
|
|
19
|
+
return (0, uuidv7_1.resolveCreateId)(attachmentId);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Client for uploading file attachments to Comms.
|
|
23
|
+
*
|
|
24
|
+
* Attachments are uploaded independently, then referenced by passing the returned
|
|
25
|
+
* {@link Attachment} into the `attachments` array of `comments.createComment`,
|
|
26
|
+
* `conversationMessages.createMessage`, and similar calls.
|
|
27
|
+
*/
|
|
28
|
+
class AttachmentsClient extends base_client_1.BaseClient {
|
|
29
|
+
/**
|
|
30
|
+
* Uploads a file and returns the created {@link Attachment}.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors the canonical multipart upload: `POST /attachments/upload` with the `file`
|
|
33
|
+
* binary plus `file_name`, `file_size`, `attachment_id`, and `underlying_type` form
|
|
34
|
+
* fields.
|
|
35
|
+
*
|
|
36
|
+
* @param args - The file to upload and optional metadata.
|
|
37
|
+
* @returns The created attachment, ready to attach to a comment or message.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { readFile } from 'node:fs/promises'
|
|
42
|
+
*
|
|
43
|
+
* const attachment = await api.attachments.upload({
|
|
44
|
+
* file: await readFile('./diagram.png'),
|
|
45
|
+
* fileName: 'diagram.png',
|
|
46
|
+
* })
|
|
47
|
+
*
|
|
48
|
+
* await api.comments.createComment({
|
|
49
|
+
* threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z',
|
|
50
|
+
* content: 'See attached',
|
|
51
|
+
* attachments: [attachment],
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
async upload(args) {
|
|
56
|
+
const data = await (0, multipart_upload_1.uploadMultipartFile)({
|
|
57
|
+
baseUrl: this.getBaseUri(),
|
|
58
|
+
authToken: this.apiToken,
|
|
59
|
+
endpoint: `${endpoints_1.ENDPOINT_ATTACHMENTS}/upload`,
|
|
60
|
+
file: args.file,
|
|
61
|
+
fileName: args.fileName,
|
|
62
|
+
contentType: args.contentType,
|
|
63
|
+
additionalFields: {
|
|
64
|
+
attachment_id: resolveAttachmentId(args.attachmentId),
|
|
65
|
+
},
|
|
66
|
+
customFetch: this.customFetch,
|
|
67
|
+
});
|
|
68
|
+
return entities_1.AttachmentSchema.parse(data);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.AttachmentsClient = AttachmentsClient;
|
package/dist/cjs/comms-api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CommsApi = void 0;
|
|
4
|
+
const attachments_client_1 = require("./clients/attachments-client");
|
|
4
5
|
const channels_client_1 = require("./clients/channels-client");
|
|
5
6
|
const comments_client_1 = require("./clients/comments-client");
|
|
6
7
|
const conversation_messages_client_1 = require("./clients/conversation-messages-client");
|
|
@@ -51,6 +52,7 @@ class CommsApi {
|
|
|
51
52
|
this.inbox = new inbox_client_1.InboxClient(clientConfig);
|
|
52
53
|
this.reactions = new reactions_client_1.ReactionsClient(clientConfig);
|
|
53
54
|
this.search = new search_client_1.SearchClient(clientConfig);
|
|
55
|
+
this.attachments = new attachments_client_1.AttachmentsClient(clientConfig);
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* Drains the SDK's process-global connection pool. CLIs and scripts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ENDPOINT_CONVERSATION_MESSAGES = exports.ENDPOINT_SEARCH = exports.ENDPOINT_REACTIONS = exports.ENDPOINT_INBOX = exports.ENDPOINT_NOTIFICATIONS = exports.ENDPOINT_COMMENTS = exports.ENDPOINT_CONVERSATIONS = exports.ENDPOINT_GROUPS = exports.ENDPOINT_THREADS = exports.ENDPOINT_CHANNELS = exports.ENDPOINT_WORKSPACES = exports.ENDPOINT_USERS = void 0;
|
|
3
|
+
exports.ENDPOINT_ATTACHMENTS = exports.ENDPOINT_CONVERSATION_MESSAGES = exports.ENDPOINT_SEARCH = exports.ENDPOINT_REACTIONS = exports.ENDPOINT_INBOX = exports.ENDPOINT_NOTIFICATIONS = exports.ENDPOINT_COMMENTS = exports.ENDPOINT_CONVERSATIONS = exports.ENDPOINT_GROUPS = exports.ENDPOINT_THREADS = exports.ENDPOINT_CHANNELS = exports.ENDPOINT_WORKSPACES = exports.ENDPOINT_USERS = void 0;
|
|
4
4
|
exports.getCommsBaseUri = getCommsBaseUri;
|
|
5
5
|
const api_version_1 = require("../types/api-version");
|
|
6
6
|
const BASE_URI = 'https://comms.todoist.com';
|
|
@@ -30,3 +30,4 @@ exports.ENDPOINT_INBOX = 'inbox';
|
|
|
30
30
|
exports.ENDPOINT_REACTIONS = 'reactions';
|
|
31
31
|
exports.ENDPOINT_SEARCH = 'search';
|
|
32
32
|
exports.ENDPOINT_CONVERSATION_MESSAGES = 'conversation_messages';
|
|
33
|
+
exports.ENDPOINT_ATTACHMENTS = 'attachments';
|
package/dist/cjs/index.js
CHANGED
|
@@ -14,8 +14,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.closeDefaultDispatcher = exports.CommsApi = exports.WorkspacesClient = exports.WorkspaceUsersClient = exports.UsersClient = exports.ThreadsClient = exports.SearchClient = exports.ReactionsClient = exports.InboxClient = exports.GroupsClient = exports.ConversationsClient = exports.ConversationMessagesClient = exports.CommentsClient = exports.ChannelsClient = void 0;
|
|
17
|
+
exports.closeDefaultDispatcher = exports.CommsApi = exports.WorkspacesClient = exports.WorkspaceUsersClient = exports.UsersClient = exports.ThreadsClient = exports.SearchClient = exports.ReactionsClient = exports.InboxClient = exports.GroupsClient = exports.ConversationsClient = exports.ConversationMessagesClient = exports.CommentsClient = exports.ChannelsClient = exports.AttachmentsClient = void 0;
|
|
18
18
|
__exportStar(require("./authentication"), exports);
|
|
19
|
+
var attachments_client_1 = require("./clients/attachments-client");
|
|
20
|
+
Object.defineProperty(exports, "AttachmentsClient", { enumerable: true, get: function () { return attachments_client_1.AttachmentsClient; } });
|
|
19
21
|
var channels_client_1 = require("./clients/channels-client");
|
|
20
22
|
Object.defineProperty(exports, "ChannelsClient", { enumerable: true, get: function () { return channels_client_1.ChannelsClient; } });
|
|
21
23
|
var comments_client_1 = require("./clients/comments-client");
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getContentTypeFromFileName = getContentTypeFromFileName;
|
|
4
|
+
exports.uploadMultipartFile = uploadMultipartFile;
|
|
5
|
+
const fetch_with_retry_1 = require("../transport/fetch-with-retry");
|
|
6
|
+
/**
|
|
7
|
+
* Determine a content-type from a filename extension. Falls back to
|
|
8
|
+
* `application/octet-stream` for unknown extensions.
|
|
9
|
+
*/
|
|
10
|
+
function getContentTypeFromFileName(fileName) {
|
|
11
|
+
const extension = fileName.toLowerCase().split('.').pop();
|
|
12
|
+
return extension ? (MIME_BY_EXTENSION[extension] ?? DEFAULT_MIME_TYPE) : DEFAULT_MIME_TYPE;
|
|
13
|
+
}
|
|
14
|
+
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
|
15
|
+
/**
|
|
16
|
+
* Extension → MIME map covering the formats commonly attached to messages.
|
|
17
|
+
* Anything not listed falls back to {@link DEFAULT_MIME_TYPE}; callers can
|
|
18
|
+
* always override via the `contentType` argument.
|
|
19
|
+
*/
|
|
20
|
+
const MIME_BY_EXTENSION = {
|
|
21
|
+
// Images
|
|
22
|
+
png: 'image/png',
|
|
23
|
+
jpg: 'image/jpeg',
|
|
24
|
+
jpeg: 'image/jpeg',
|
|
25
|
+
gif: 'image/gif',
|
|
26
|
+
webp: 'image/webp',
|
|
27
|
+
svg: 'image/svg+xml',
|
|
28
|
+
bmp: 'image/bmp',
|
|
29
|
+
ico: 'image/x-icon',
|
|
30
|
+
tif: 'image/tiff',
|
|
31
|
+
tiff: 'image/tiff',
|
|
32
|
+
heic: 'image/heic',
|
|
33
|
+
heif: 'image/heif',
|
|
34
|
+
avif: 'image/avif',
|
|
35
|
+
// Documents / text
|
|
36
|
+
pdf: 'application/pdf',
|
|
37
|
+
txt: 'text/plain',
|
|
38
|
+
md: 'text/markdown',
|
|
39
|
+
csv: 'text/csv',
|
|
40
|
+
json: 'application/json',
|
|
41
|
+
xml: 'application/xml',
|
|
42
|
+
html: 'text/html',
|
|
43
|
+
htm: 'text/html',
|
|
44
|
+
rtf: 'application/rtf',
|
|
45
|
+
doc: 'application/msword',
|
|
46
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
47
|
+
xls: 'application/vnd.ms-excel',
|
|
48
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
49
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
50
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
51
|
+
// Archives
|
|
52
|
+
zip: 'application/zip',
|
|
53
|
+
gz: 'application/gzip',
|
|
54
|
+
tar: 'application/x-tar',
|
|
55
|
+
'7z': 'application/x-7z-compressed',
|
|
56
|
+
rar: 'application/vnd.rar',
|
|
57
|
+
// Audio
|
|
58
|
+
mp3: 'audio/mpeg',
|
|
59
|
+
wav: 'audio/wav',
|
|
60
|
+
ogg: 'audio/ogg',
|
|
61
|
+
m4a: 'audio/mp4',
|
|
62
|
+
flac: 'audio/flac',
|
|
63
|
+
aac: 'audio/aac',
|
|
64
|
+
// Video
|
|
65
|
+
mp4: 'video/mp4',
|
|
66
|
+
m4v: 'video/mp4',
|
|
67
|
+
mov: 'video/quicktime',
|
|
68
|
+
avi: 'video/x-msvideo',
|
|
69
|
+
webm: 'video/webm',
|
|
70
|
+
mkv: 'video/x-matroska',
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Normalise a supported {@link UploadFile} into a `Blob` plus a resolved file name and
|
|
74
|
+
* content type, so uploads use the cross-platform global `FormData`/`Blob` body that
|
|
75
|
+
* `undici` and browsers both accept natively.
|
|
76
|
+
*/
|
|
77
|
+
function toBlob(file, fileName, contentType) {
|
|
78
|
+
if (file instanceof Blob) {
|
|
79
|
+
// Duck-type the name rather than `instanceof File`: `File` isn't a global in
|
|
80
|
+
// Node 18, so a `node:buffer` `File` (which carries a real `name`) would
|
|
81
|
+
// otherwise fall back to `'upload'` and lose its inferred file name.
|
|
82
|
+
const blobName = file.name;
|
|
83
|
+
const name = fileName || (typeof blobName === 'string' ? blobName : undefined) || 'upload';
|
|
84
|
+
const type = contentType || file.type || getContentTypeFromFileName(name);
|
|
85
|
+
// Re-wrap only when stamping a type the Blob doesn't already carry.
|
|
86
|
+
const blob = file.type === type ? file : new Blob([file], { type });
|
|
87
|
+
return { blob, fileName: name, contentType: type };
|
|
88
|
+
}
|
|
89
|
+
if (file instanceof Uint8Array) {
|
|
90
|
+
if (!fileName) {
|
|
91
|
+
throw new Error('fileName is required when uploading raw bytes');
|
|
92
|
+
}
|
|
93
|
+
const type = contentType || getContentTypeFromFileName(fileName);
|
|
94
|
+
// `Blob` accepts any `ArrayBufferView`; the cast satisfies the stricter lib
|
|
95
|
+
// `BlobPart` type (which pins the backing buffer to `ArrayBuffer`).
|
|
96
|
+
return { blob: new Blob([file], { type }), fileName, contentType: type };
|
|
97
|
+
}
|
|
98
|
+
throw new Error('Unsupported file type for upload: expected a Blob or Uint8Array');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Upload a file using `multipart/form-data`.
|
|
102
|
+
*
|
|
103
|
+
* Builds the request body with the global `FormData`/`Blob` so it works unchanged in the
|
|
104
|
+
* browser and in Node.js (via `undici`). The `file` part is sent alongside `file_name`,
|
|
105
|
+
* `file_size`, and `underlying_type` fields (the canonical Comms upload shape); any
|
|
106
|
+
* `additionalFields` are merged in and override the derived values. Authentication uses
|
|
107
|
+
* `Authorization: Bearer`, matching every other Comms SDK client, and `Content-Type` is
|
|
108
|
+
* intentionally left unset so the runtime adds the correct multipart boundary.
|
|
109
|
+
*
|
|
110
|
+
* The response is JSON-parsed and camel-cased by {@link fetchWithRetry}; callers validate
|
|
111
|
+
* the returned shape with the appropriate schema.
|
|
112
|
+
*/
|
|
113
|
+
async function uploadMultipartFile(args) {
|
|
114
|
+
const { baseUrl, authToken, endpoint, file, fileName, contentType, additionalFields, maxRetries = 0, requestId, customFetch, } = args;
|
|
115
|
+
const { blob, fileName: resolvedFileName, contentType: resolvedType, } = toBlob(file, fileName, contentType);
|
|
116
|
+
const fields = {
|
|
117
|
+
file_name: resolvedFileName,
|
|
118
|
+
file_size: blob.size,
|
|
119
|
+
underlying_type: resolvedType,
|
|
120
|
+
...additionalFields,
|
|
121
|
+
};
|
|
122
|
+
const form = new FormData();
|
|
123
|
+
form.append('file', blob, resolvedFileName);
|
|
124
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
125
|
+
if (value !== undefined && value !== null) {
|
|
126
|
+
form.append(key, String(value));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const headers = {
|
|
130
|
+
Authorization: `Bearer ${authToken}`,
|
|
131
|
+
};
|
|
132
|
+
if (requestId) {
|
|
133
|
+
headers['X-Request-Id'] = requestId;
|
|
134
|
+
}
|
|
135
|
+
const url = new URL(endpoint, baseUrl).toString();
|
|
136
|
+
const response = await (0, fetch_with_retry_1.fetchWithRetry)(url, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers,
|
|
139
|
+
body: form,
|
|
140
|
+
// Don't set Content-Type — the runtime adds the multipart boundary.
|
|
141
|
+
timeout: 30000,
|
|
142
|
+
}, maxRetries, customFetch);
|
|
143
|
+
return response.data;
|
|
144
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ENDPOINT_ATTACHMENTS } from '../consts/endpoints.js';
|
|
2
|
+
import { AttachmentSchema } from '../types/entities.js';
|
|
3
|
+
import { uploadMultipartFile } from '../utils/multipart-upload.js';
|
|
4
|
+
import { isValidUuidV7Base58, resolveCreateId, UuidV7Error } from '../utils/uuidv7.js';
|
|
5
|
+
import { BaseClient } from './base-client.js';
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the `attachment_id`: validate a caller-supplied value (throwing an
|
|
8
|
+
* {@link UuidV7Error} that names `attachmentId` rather than the generic `id`) or
|
|
9
|
+
* mint a fresh one. Wraps {@link resolveCreateId} so the error contract matches the
|
|
10
|
+
* client's actual parameter.
|
|
11
|
+
*/
|
|
12
|
+
function resolveAttachmentId(attachmentId) {
|
|
13
|
+
if (attachmentId !== undefined && !isValidUuidV7Base58(attachmentId)) {
|
|
14
|
+
throw new UuidV7Error(`invalid attachmentId ${JSON.stringify(attachmentId)} — use generateId() or omit \`attachmentId\` and let the SDK mint one.`);
|
|
15
|
+
}
|
|
16
|
+
return resolveCreateId(attachmentId);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Client for uploading file attachments to Comms.
|
|
20
|
+
*
|
|
21
|
+
* Attachments are uploaded independently, then referenced by passing the returned
|
|
22
|
+
* {@link Attachment} into the `attachments` array of `comments.createComment`,
|
|
23
|
+
* `conversationMessages.createMessage`, and similar calls.
|
|
24
|
+
*/
|
|
25
|
+
export class AttachmentsClient extends BaseClient {
|
|
26
|
+
/**
|
|
27
|
+
* Uploads a file and returns the created {@link Attachment}.
|
|
28
|
+
*
|
|
29
|
+
* Mirrors the canonical multipart upload: `POST /attachments/upload` with the `file`
|
|
30
|
+
* binary plus `file_name`, `file_size`, `attachment_id`, and `underlying_type` form
|
|
31
|
+
* fields.
|
|
32
|
+
*
|
|
33
|
+
* @param args - The file to upload and optional metadata.
|
|
34
|
+
* @returns The created attachment, ready to attach to a comment or message.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { readFile } from 'node:fs/promises'
|
|
39
|
+
*
|
|
40
|
+
* const attachment = await api.attachments.upload({
|
|
41
|
+
* file: await readFile('./diagram.png'),
|
|
42
|
+
* fileName: 'diagram.png',
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* await api.comments.createComment({
|
|
46
|
+
* threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z',
|
|
47
|
+
* content: 'See attached',
|
|
48
|
+
* attachments: [attachment],
|
|
49
|
+
* })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
async upload(args) {
|
|
53
|
+
const data = await uploadMultipartFile({
|
|
54
|
+
baseUrl: this.getBaseUri(),
|
|
55
|
+
authToken: this.apiToken,
|
|
56
|
+
endpoint: `${ENDPOINT_ATTACHMENTS}/upload`,
|
|
57
|
+
file: args.file,
|
|
58
|
+
fileName: args.fileName,
|
|
59
|
+
contentType: args.contentType,
|
|
60
|
+
additionalFields: {
|
|
61
|
+
attachment_id: resolveAttachmentId(args.attachmentId),
|
|
62
|
+
},
|
|
63
|
+
customFetch: this.customFetch,
|
|
64
|
+
});
|
|
65
|
+
return AttachmentSchema.parse(data);
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/esm/comms-api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AttachmentsClient } from './clients/attachments-client.js';
|
|
1
2
|
import { ChannelsClient } from './clients/channels-client.js';
|
|
2
3
|
import { CommentsClient } from './clients/comments-client.js';
|
|
3
4
|
import { ConversationMessagesClient } from './clients/conversation-messages-client.js';
|
|
@@ -48,6 +49,7 @@ export class CommsApi {
|
|
|
48
49
|
this.inbox = new InboxClient(clientConfig);
|
|
49
50
|
this.reactions = new ReactionsClient(clientConfig);
|
|
50
51
|
this.search = new SearchClient(clientConfig);
|
|
52
|
+
this.attachments = new AttachmentsClient(clientConfig);
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
55
|
* Drains the SDK's process-global connection pool. CLIs and scripts
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './authentication.js';
|
|
2
|
+
export { AttachmentsClient } from './clients/attachments-client.js';
|
|
2
3
|
export { ChannelsClient } from './clients/channels-client.js';
|
|
3
4
|
export { CommentsClient } from './clients/comments-client.js';
|
|
4
5
|
export { ConversationMessagesClient } from './clients/conversation-messages-client.js';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { fetchWithRetry } from '../transport/fetch-with-retry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Determine a content-type from a filename extension. Falls back to
|
|
4
|
+
* `application/octet-stream` for unknown extensions.
|
|
5
|
+
*/
|
|
6
|
+
export function getContentTypeFromFileName(fileName) {
|
|
7
|
+
const extension = fileName.toLowerCase().split('.').pop();
|
|
8
|
+
return extension ? (MIME_BY_EXTENSION[extension] ?? DEFAULT_MIME_TYPE) : DEFAULT_MIME_TYPE;
|
|
9
|
+
}
|
|
10
|
+
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
|
11
|
+
/**
|
|
12
|
+
* Extension → MIME map covering the formats commonly attached to messages.
|
|
13
|
+
* Anything not listed falls back to {@link DEFAULT_MIME_TYPE}; callers can
|
|
14
|
+
* always override via the `contentType` argument.
|
|
15
|
+
*/
|
|
16
|
+
const MIME_BY_EXTENSION = {
|
|
17
|
+
// Images
|
|
18
|
+
png: 'image/png',
|
|
19
|
+
jpg: 'image/jpeg',
|
|
20
|
+
jpeg: 'image/jpeg',
|
|
21
|
+
gif: 'image/gif',
|
|
22
|
+
webp: 'image/webp',
|
|
23
|
+
svg: 'image/svg+xml',
|
|
24
|
+
bmp: 'image/bmp',
|
|
25
|
+
ico: 'image/x-icon',
|
|
26
|
+
tif: 'image/tiff',
|
|
27
|
+
tiff: 'image/tiff',
|
|
28
|
+
heic: 'image/heic',
|
|
29
|
+
heif: 'image/heif',
|
|
30
|
+
avif: 'image/avif',
|
|
31
|
+
// Documents / text
|
|
32
|
+
pdf: 'application/pdf',
|
|
33
|
+
txt: 'text/plain',
|
|
34
|
+
md: 'text/markdown',
|
|
35
|
+
csv: 'text/csv',
|
|
36
|
+
json: 'application/json',
|
|
37
|
+
xml: 'application/xml',
|
|
38
|
+
html: 'text/html',
|
|
39
|
+
htm: 'text/html',
|
|
40
|
+
rtf: 'application/rtf',
|
|
41
|
+
doc: 'application/msword',
|
|
42
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
43
|
+
xls: 'application/vnd.ms-excel',
|
|
44
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
45
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
46
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
47
|
+
// Archives
|
|
48
|
+
zip: 'application/zip',
|
|
49
|
+
gz: 'application/gzip',
|
|
50
|
+
tar: 'application/x-tar',
|
|
51
|
+
'7z': 'application/x-7z-compressed',
|
|
52
|
+
rar: 'application/vnd.rar',
|
|
53
|
+
// Audio
|
|
54
|
+
mp3: 'audio/mpeg',
|
|
55
|
+
wav: 'audio/wav',
|
|
56
|
+
ogg: 'audio/ogg',
|
|
57
|
+
m4a: 'audio/mp4',
|
|
58
|
+
flac: 'audio/flac',
|
|
59
|
+
aac: 'audio/aac',
|
|
60
|
+
// Video
|
|
61
|
+
mp4: 'video/mp4',
|
|
62
|
+
m4v: 'video/mp4',
|
|
63
|
+
mov: 'video/quicktime',
|
|
64
|
+
avi: 'video/x-msvideo',
|
|
65
|
+
webm: 'video/webm',
|
|
66
|
+
mkv: 'video/x-matroska',
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Normalise a supported {@link UploadFile} into a `Blob` plus a resolved file name and
|
|
70
|
+
* content type, so uploads use the cross-platform global `FormData`/`Blob` body that
|
|
71
|
+
* `undici` and browsers both accept natively.
|
|
72
|
+
*/
|
|
73
|
+
function toBlob(file, fileName, contentType) {
|
|
74
|
+
if (file instanceof Blob) {
|
|
75
|
+
// Duck-type the name rather than `instanceof File`: `File` isn't a global in
|
|
76
|
+
// Node 18, so a `node:buffer` `File` (which carries a real `name`) would
|
|
77
|
+
// otherwise fall back to `'upload'` and lose its inferred file name.
|
|
78
|
+
const blobName = file.name;
|
|
79
|
+
const name = fileName || (typeof blobName === 'string' ? blobName : undefined) || 'upload';
|
|
80
|
+
const type = contentType || file.type || getContentTypeFromFileName(name);
|
|
81
|
+
// Re-wrap only when stamping a type the Blob doesn't already carry.
|
|
82
|
+
const blob = file.type === type ? file : new Blob([file], { type });
|
|
83
|
+
return { blob, fileName: name, contentType: type };
|
|
84
|
+
}
|
|
85
|
+
if (file instanceof Uint8Array) {
|
|
86
|
+
if (!fileName) {
|
|
87
|
+
throw new Error('fileName is required when uploading raw bytes');
|
|
88
|
+
}
|
|
89
|
+
const type = contentType || getContentTypeFromFileName(fileName);
|
|
90
|
+
// `Blob` accepts any `ArrayBufferView`; the cast satisfies the stricter lib
|
|
91
|
+
// `BlobPart` type (which pins the backing buffer to `ArrayBuffer`).
|
|
92
|
+
return { blob: new Blob([file], { type }), fileName, contentType: type };
|
|
93
|
+
}
|
|
94
|
+
throw new Error('Unsupported file type for upload: expected a Blob or Uint8Array');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Upload a file using `multipart/form-data`.
|
|
98
|
+
*
|
|
99
|
+
* Builds the request body with the global `FormData`/`Blob` so it works unchanged in the
|
|
100
|
+
* browser and in Node.js (via `undici`). The `file` part is sent alongside `file_name`,
|
|
101
|
+
* `file_size`, and `underlying_type` fields (the canonical Comms upload shape); any
|
|
102
|
+
* `additionalFields` are merged in and override the derived values. Authentication uses
|
|
103
|
+
* `Authorization: Bearer`, matching every other Comms SDK client, and `Content-Type` is
|
|
104
|
+
* intentionally left unset so the runtime adds the correct multipart boundary.
|
|
105
|
+
*
|
|
106
|
+
* The response is JSON-parsed and camel-cased by {@link fetchWithRetry}; callers validate
|
|
107
|
+
* the returned shape with the appropriate schema.
|
|
108
|
+
*/
|
|
109
|
+
export async function uploadMultipartFile(args) {
|
|
110
|
+
const { baseUrl, authToken, endpoint, file, fileName, contentType, additionalFields, maxRetries = 0, requestId, customFetch, } = args;
|
|
111
|
+
const { blob, fileName: resolvedFileName, contentType: resolvedType, } = toBlob(file, fileName, contentType);
|
|
112
|
+
const fields = {
|
|
113
|
+
file_name: resolvedFileName,
|
|
114
|
+
file_size: blob.size,
|
|
115
|
+
underlying_type: resolvedType,
|
|
116
|
+
...additionalFields,
|
|
117
|
+
};
|
|
118
|
+
const form = new FormData();
|
|
119
|
+
form.append('file', blob, resolvedFileName);
|
|
120
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
121
|
+
if (value !== undefined && value !== null) {
|
|
122
|
+
form.append(key, String(value));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const headers = {
|
|
126
|
+
Authorization: `Bearer ${authToken}`,
|
|
127
|
+
};
|
|
128
|
+
if (requestId) {
|
|
129
|
+
headers['X-Request-Id'] = requestId;
|
|
130
|
+
}
|
|
131
|
+
const url = new URL(endpoint, baseUrl).toString();
|
|
132
|
+
const response = await fetchWithRetry(url, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers,
|
|
135
|
+
body: form,
|
|
136
|
+
// Don't set Content-Type — the runtime adds the multipart boundary.
|
|
137
|
+
timeout: 30000,
|
|
138
|
+
}, maxRetries, customFetch);
|
|
139
|
+
return response.data;
|
|
140
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Attachment } from '../types/entities.js';
|
|
2
|
+
import type { UploadAttachmentArgs } from '../types/requests.js';
|
|
3
|
+
import { BaseClient } from './base-client.js';
|
|
4
|
+
/**
|
|
5
|
+
* Client for uploading file attachments to Comms.
|
|
6
|
+
*
|
|
7
|
+
* Attachments are uploaded independently, then referenced by passing the returned
|
|
8
|
+
* {@link Attachment} into the `attachments` array of `comments.createComment`,
|
|
9
|
+
* `conversationMessages.createMessage`, and similar calls.
|
|
10
|
+
*/
|
|
11
|
+
export declare class AttachmentsClient extends BaseClient {
|
|
12
|
+
/**
|
|
13
|
+
* Uploads a file and returns the created {@link Attachment}.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the canonical multipart upload: `POST /attachments/upload` with the `file`
|
|
16
|
+
* binary plus `file_name`, `file_size`, `attachment_id`, and `underlying_type` form
|
|
17
|
+
* fields.
|
|
18
|
+
*
|
|
19
|
+
* @param args - The file to upload and optional metadata.
|
|
20
|
+
* @returns The created attachment, ready to attach to a comment or message.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { readFile } from 'node:fs/promises'
|
|
25
|
+
*
|
|
26
|
+
* const attachment = await api.attachments.upload({
|
|
27
|
+
* file: await readFile('./diagram.png'),
|
|
28
|
+
* fileName: 'diagram.png',
|
|
29
|
+
* })
|
|
30
|
+
*
|
|
31
|
+
* await api.comments.createComment({
|
|
32
|
+
* threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z',
|
|
33
|
+
* content: 'See attached',
|
|
34
|
+
* attachments: [attachment],
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
upload(args: UploadAttachmentArgs): Promise<Attachment>;
|
|
39
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AttachmentsClient } from './clients/attachments-client.js';
|
|
1
2
|
import { ChannelsClient } from './clients/channels-client.js';
|
|
2
3
|
import { CommentsClient } from './clients/comments-client.js';
|
|
3
4
|
import { ConversationMessagesClient } from './clients/conversation-messages-client.js';
|
|
@@ -44,6 +45,7 @@ export declare class CommsApi {
|
|
|
44
45
|
inbox: InboxClient;
|
|
45
46
|
reactions: ReactionsClient;
|
|
46
47
|
search: SearchClient;
|
|
48
|
+
attachments: AttachmentsClient;
|
|
47
49
|
/**
|
|
48
50
|
* Creates a new Comms API client.
|
|
49
51
|
*
|
|
@@ -22,3 +22,4 @@ export declare const ENDPOINT_INBOX = "inbox";
|
|
|
22
22
|
export declare const ENDPOINT_REACTIONS = "reactions";
|
|
23
23
|
export declare const ENDPOINT_SEARCH = "search";
|
|
24
24
|
export declare const ENDPOINT_CONVERSATION_MESSAGES = "conversation_messages";
|
|
25
|
+
export declare const ENDPOINT_ATTACHMENTS = "attachments";
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from './authentication.js';
|
|
2
|
+
export { AttachmentsClient } from './clients/attachments-client.js';
|
|
2
3
|
export { ChannelsClient } from './clients/channels-client.js';
|
|
3
4
|
export { CommentsClient } from './clients/comments-client.js';
|
|
4
5
|
export { ConversationMessagesClient } from './clients/conversation-messages-client.js';
|
|
@@ -368,4 +368,26 @@ export type GetUserLocalTimeArgs = {
|
|
|
368
368
|
workspaceId: number;
|
|
369
369
|
userId: number;
|
|
370
370
|
};
|
|
371
|
+
type UploadAttachmentCommonArgs = {
|
|
372
|
+
/** MIME type. Defaults to the `Blob`'s type or one inferred from the file extension. */
|
|
373
|
+
contentType?: string;
|
|
374
|
+
/** Attachment ID to use. A random ID is generated when omitted. */
|
|
375
|
+
attachmentId?: string;
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Arguments for `attachments.upload`. A discriminated union on `file` enforces, at
|
|
379
|
+
* compile time, that raw `Uint8Array` bytes are accompanied by a `fileName` (there is
|
|
380
|
+
* no name to infer), while a `Blob`/`File` may omit it.
|
|
381
|
+
*/
|
|
382
|
+
export type UploadAttachmentArgs = UploadAttachmentCommonArgs & ({
|
|
383
|
+
/** The file to upload — a `Blob`/`File` (browser, or any runtime with a global `Blob`). */
|
|
384
|
+
file: Blob;
|
|
385
|
+
/** File name. Inferred from the `File.name` when omitted. */
|
|
386
|
+
fileName?: string;
|
|
387
|
+
} | {
|
|
388
|
+
/** Raw bytes to upload. A Node `Buffer` is a `Uint8Array`, so `await readFile(path)` works directly. */
|
|
389
|
+
file: Uint8Array;
|
|
390
|
+
/** File name. Required for raw bytes — there is no name to infer. */
|
|
391
|
+
fileName: string;
|
|
392
|
+
});
|
|
371
393
|
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CustomFetch } from '../types/http.js';
|
|
2
|
+
/**
|
|
3
|
+
* File content accepted by the upload helpers.
|
|
4
|
+
*
|
|
5
|
+
* - `Blob`/`File` — browser, or any runtime with a global `Blob` (Node 18+).
|
|
6
|
+
* - `Uint8Array` — raw bytes (requires `fileName`). A Node `Buffer` is a `Uint8Array`,
|
|
7
|
+
* so reading a file with `fs` and passing the result works without conversion.
|
|
8
|
+
*
|
|
9
|
+
* Both are universal types, so the published `.d.ts` stays free of Node-only globals
|
|
10
|
+
* (`Buffer`, `NodeJS.*`) and the helper pulls in no Node built-ins — keeping the browser
|
|
11
|
+
* bundle clean.
|
|
12
|
+
*/
|
|
13
|
+
export type UploadFile = Blob | Uint8Array;
|
|
14
|
+
type UploadMultipartFileArgs = {
|
|
15
|
+
/** Base API URI with trailing slash, e.g. `https://comms.todoist.com/api/v1/`. */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
/** API token used for `Authorization: Bearer`. */
|
|
18
|
+
authToken: string;
|
|
19
|
+
/** Relative endpoint path, e.g. `attachments/upload`. */
|
|
20
|
+
endpoint: string;
|
|
21
|
+
/** File content to upload. */
|
|
22
|
+
file: UploadFile;
|
|
23
|
+
/** File name. Required for raw `Uint8Array` bytes; inferred from a `File`. */
|
|
24
|
+
fileName?: string;
|
|
25
|
+
/** MIME type. Defaults to the `Blob`'s type or one inferred from the file extension. */
|
|
26
|
+
contentType?: string;
|
|
27
|
+
/** Extra multipart fields to send alongside the file metadata fields. */
|
|
28
|
+
additionalFields?: Record<string, string | number | boolean | undefined | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Network-error retry count. Defaults to `0` — unlike JSON requests, a retry here
|
|
31
|
+
* resends the entire file body, so large uploads aren't retried automatically.
|
|
32
|
+
*/
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
/** Optional request ID for tracing. */
|
|
35
|
+
requestId?: string;
|
|
36
|
+
/** Optional custom fetch implementation. */
|
|
37
|
+
customFetch?: CustomFetch;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Determine a content-type from a filename extension. Falls back to
|
|
41
|
+
* `application/octet-stream` for unknown extensions.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getContentTypeFromFileName(fileName: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Upload a file using `multipart/form-data`.
|
|
46
|
+
*
|
|
47
|
+
* Builds the request body with the global `FormData`/`Blob` so it works unchanged in the
|
|
48
|
+
* browser and in Node.js (via `undici`). The `file` part is sent alongside `file_name`,
|
|
49
|
+
* `file_size`, and `underlying_type` fields (the canonical Comms upload shape); any
|
|
50
|
+
* `additionalFields` are merged in and override the derived values. Authentication uses
|
|
51
|
+
* `Authorization: Bearer`, matching every other Comms SDK client, and `Content-Type` is
|
|
52
|
+
* intentionally left unset so the runtime adds the correct multipart boundary.
|
|
53
|
+
*
|
|
54
|
+
* The response is JSON-parsed and camel-cased by {@link fetchWithRetry}; callers validate
|
|
55
|
+
* the returned shape with the appropriate schema.
|
|
56
|
+
*/
|
|
57
|
+
export declare function uploadMultipartFile<T>(args: UploadMultipartFileArgs): Promise<T>;
|
|
58
|
+
export {};
|