From ee4bfae0ace0f1bfb7162388e6dea7e87594e55e Mon Sep 17 00:00:00 2001 From: Amir Date: Thu, 21 May 2026 13:01:35 +0200 Subject: [PATCH 1/5] feat: restore configurable API version option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2 hardcoded `v1` everywhere. Per Scott's review, the right move was to change the default — not drop the option. Restores `version?: ApiVersion` on `ClientConfig` / `CommsApiOptions`, defaulting to `'v1'`. The `ApiVersion` union currently lists only `'v1'`; new versions just need a one-line entry in `API_VERSIONS`. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/clients/base-client.ts | 10 ++++++++-- src/comms-api.ts | 4 ++++ src/consts/endpoints.ts | 12 +++++++++--- src/types/api-version.ts | 7 +++++++ src/types/index.ts | 1 + 5 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 src/types/api-version.ts diff --git a/src/clients/base-client.ts b/src/clients/base-client.ts index 5697063..c071430 100644 --- a/src/clients/base-client.ts +++ b/src/clients/base-client.ts @@ -1,4 +1,6 @@ import { getCommsBaseUri } from '../consts/endpoints' +import type { ApiVersion } from '../types/api-version' +import { DEFAULT_API_VERSION } from '../types/api-version' import type { CustomFetch } from '../types/http' export type ClientConfig = { @@ -6,6 +8,8 @@ export type ClientConfig = { apiToken: string /** Optional custom base URL. If not provided, uses the default Comms API URL */ baseUrl?: string + /** Optional API version. Defaults to 'v1' */ + version?: ApiVersion /** Optional custom fetch implementation for cross-platform compatibility */ customFetch?: CustomFetch } @@ -17,11 +21,13 @@ export type ClientConfig = { export class BaseClient { protected readonly apiToken: string protected readonly baseUrl?: string + protected readonly defaultVersion: ApiVersion protected readonly customFetch?: CustomFetch constructor(config: ClientConfig) { this.apiToken = config.apiToken this.baseUrl = config.baseUrl + this.defaultVersion = config.version || DEFAULT_API_VERSION this.customFetch = config.customFetch } @@ -32,8 +38,8 @@ export class BaseClient { protected getBaseUri(): string { if (this.baseUrl) { const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/` - return `${normalizedBaseUrl}api/v1/` + return `${normalizedBaseUrl}api/${this.defaultVersion}/` } - return getCommsBaseUri() + return getCommsBaseUri(this.defaultVersion) } } diff --git a/src/comms-api.ts b/src/comms-api.ts index 602a132..d53ed17 100644 --- a/src/comms-api.ts +++ b/src/comms-api.ts @@ -11,11 +11,14 @@ import { UsersClient } from './clients/users-client' import { WorkspaceUsersClient } from './clients/workspace-users-client' import { WorkspacesClient } from './clients/workspaces-client' import { closeDefaultDispatcher } from './transport/http-dispatcher' +import type { ApiVersion } from './types/api-version' import type { CustomFetch } from './types/http' export type CommsApiOptions = { /** Optional custom API base URL. If not provided, defaults to Comms' standard API endpoint. */ baseUrl?: string + /** Optional API version. Defaults to 'v1'. */ + version?: ApiVersion /** Optional custom fetch implementation for cross-platform compatibility (e.g., Obsidian, React Native, Electron). */ customFetch?: CustomFetch } @@ -55,6 +58,7 @@ export class CommsApi { const clientConfig = { apiToken: authToken, baseUrl: options?.baseUrl, + version: options?.version, customFetch: options?.customFetch, } diff --git a/src/consts/endpoints.ts b/src/consts/endpoints.ts index a61507d..d98463c 100644 --- a/src/consts/endpoints.ts +++ b/src/consts/endpoints.ts @@ -1,14 +1,20 @@ +import type { ApiVersion } from '../types/api-version' +import { DEFAULT_API_VERSION } from '../types/api-version' + const BASE_URI = 'https://comms.todoist.com' -const API_VERSION = 'v1' /** * Gets the base URI for Comms API requests. * + * @param version - API version. Defaults to 'v1'. * @param domainBase - Custom domain base URL. Defaults to Comms' API domain. * @returns Complete base URI with trailing slash (e.g., 'https://comms.todoist.com/api/v1/') */ -export function getCommsBaseUri(domainBase: string = BASE_URI): string { - return new URL(`/api/${API_VERSION}/`, domainBase).toString() +export function getCommsBaseUri( + version: ApiVersion = DEFAULT_API_VERSION, + domainBase: string = BASE_URI, +): string { + return new URL(`/api/${version}/`, domainBase).toString() } export const ENDPOINT_USERS = 'users' diff --git a/src/types/api-version.ts b/src/types/api-version.ts new file mode 100644 index 0000000..891d2cb --- /dev/null +++ b/src/types/api-version.ts @@ -0,0 +1,7 @@ +/** + * Supported Comms API versions + */ +export const API_VERSIONS = ['v1'] as const +export type ApiVersion = (typeof API_VERSIONS)[number] + +export const DEFAULT_API_VERSION: ApiVersion = 'v1' diff --git a/src/types/index.ts b/src/types/index.ts index 5d569ca..0bb6e14 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './api-version' export * from './entities' export * from './enums' export * from './errors' From 4e279ce38122bd26a27ed7bffed7df3eb8527096 Mon Sep 17 00:00:00 2001 From: Amir Date: Thu, 21 May 2026 13:01:49 +0200 Subject: [PATCH 2/5] docs: restore detailed JSDoc on client methods PR #2 stripped `@param` / `@returns` / `@example` blocks as collateral damage while collapsing the batch overloads. Per Scott's review, those JSDocs feed the SDK documentation site. Restored from the bootstrap SDK and re-anchored onto the current single-signature methods, with all batch references dropped. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/clients/add-comment-helper.ts | 17 ++ src/clients/channels-client.ts | 129 +++++++++++- src/clients/comments-client.ts | 82 +++++++- src/clients/conversation-messages-client.ts | 81 +++++++- src/clients/conversations-client.ts | 146 +++++++++++++- src/clients/groups-client.ts | 93 ++++++++- src/clients/inbox-client.ts | 90 ++++++++- src/clients/reactions-client.ts | 42 +++- src/clients/search-client.ts | 50 +++++ src/clients/threads-client.ts | 208 +++++++++++++++++++- src/clients/users-client.ts | 148 ++++++++++++-- src/clients/workspace-users-client.ts | 120 ++++++++++- src/clients/workspaces-client.ts | 87 +++++++- 13 files changed, 1225 insertions(+), 68 deletions(-) diff --git a/src/clients/add-comment-helper.ts b/src/clients/add-comment-helper.ts index c55bd9e..f85e491 100644 --- a/src/clients/add-comment-helper.ts +++ b/src/clients/add-comment-helper.ts @@ -55,6 +55,23 @@ function applyNotifyAudience(params: CreateCommentArgs): Omit console.log(ch.name)) + * ``` + */ getChannels(args: GetChannelsArgs): Promise { return request({ httpMethod: 'GET', @@ -34,22 +47,64 @@ export class ChannelsClient extends BaseClient { }).then((response) => ChannelListSchema.parse(response.data)) } - /** Fetches a single channel by ID. */ + /** + * Gets a single channel object by id. + * + * @param id - The channel ID. + * @returns The channel object. + */ getChannel(id: string): Promise { return this.simple('GET', 'getone', { id }, ChannelSchema) } - /** Creates a new channel. `id` is auto-generated if not supplied. */ + /** + * Creates a new channel. `id` is auto-generated if not supplied — pass your + * own `id` to keep an optimistic-UI ID stable through the round-trip. + * + * @param args - The arguments for creating a channel. + * @param args.workspaceId - The workspace ID. + * @param args.name - The channel name. + * @param args.description - Optional channel description. + * @param args.color - Optional channel color. + * @param args.userIds - Optional array of user IDs to add to the channel. + * @param args.public - Optional flag to make the channel public. + * @returns The created channel object. + * + * @example + * ```typescript + * const channel = await api.channels.createChannel({ + * workspaceId: 123, + * name: 'Engineering', + * description: 'Engineering team channel', + * }) + * ``` + */ createChannel(args: CreateChannelArgs): Promise { return this.simple('POST', 'add', { ...args, id: resolveCreateId(args.id) }, ChannelSchema) } - /** Partial update of an existing channel. */ + /** + * Partial update of an existing channel. + * + * @param args - The arguments for updating a channel. + * @param args.id - The channel ID. + * @param args.name - Optional new channel name. + * @param args.description - Optional new channel description. + * @param args.color - Optional new channel color. + * @param args.public - Optional flag to change channel visibility. + * @returns The updated channel object. + */ updateChannel(args: UpdateChannelArgs): Promise { return this.simple('POST', 'update', { ...args }, ChannelSchema) } - /** Updates the channel's view filter (`only_open` / `all` / `only_closed`). */ + /** + * Updates the channel's view filter (`only_open` / `all` / `only_closed`). + * + * @param args - The arguments for updating the channel filter. + * @param args.id - The channel ID. + * @param args.filterClosed - The new filter value. + */ updateFilters(args: { id: string filterClosed: 'only_open' | 'all' | 'only_closed' @@ -57,39 +112,101 @@ export class ChannelsClient extends BaseClient { return this.simple('POST', 'update_filters', { ...args }, StatusOkSchema) } - /** Permanently deletes a channel. */ + /** + * Permanently deletes a channel. + * + * @param id - The channel ID. + */ deleteChannel(id: string): Promise { return this.simple('POST', 'remove', { id }, StatusOkSchema) } + /** + * Archives a channel. + * + * @param id - The channel ID. + */ archiveChannel(id: string): Promise { return this.simple('POST', 'archive', { id }, StatusOkSchema) } + /** + * Unarchives a channel. + * + * @param id - The channel ID. + */ unarchiveChannel(id: string): Promise { return this.simple('POST', 'unarchive', { id }, StatusOkSchema) } + /** + * Favorites a channel. + * + * @param id - The channel ID. + */ favoriteChannel(id: string): Promise { return this.simple('POST', 'favorite', { id }, StatusOkSchema) } + /** + * Unfavorites a channel. + * + * @param id - The channel ID. + */ unfavoriteChannel(id: string): Promise { return this.simple('POST', 'unfavorite', { id }, StatusOkSchema) } + /** + * Adds a user to a channel. + * + * @param args - The arguments for adding a user. + * @param args.id - The channel ID. + * @param args.userId - The user ID to add. + * + * @example + * ```typescript + * await api.channels.addUser({ id: '7YpL3oZ4kZ9vP7Q1tR2sX44', userId: 101 }) + * ``` + */ addUser(args: AddChannelUserArgs): Promise { return this.simple('POST', 'add_user', { ...args }, ChannelSchema) } + /** + * Adds multiple users to a channel. + * + * @param args - The arguments for adding users. + * @param args.id - The channel ID. + * @param args.userIds - Array of user IDs to add. + * + * @example + * ```typescript + * await api.channels.addUsers({ id: '7YpL3oZ4kZ9vP7Q1tR2sX44', userIds: [101, 202] }) + * ``` + */ addUsers(args: AddChannelUsersArgs): Promise { return this.simple('POST', 'add_users', { ...args }, ChannelSchema) } + /** + * Removes a user from a channel. + * + * @param args - The arguments for removing a user. + * @param args.id - The channel ID. + * @param args.userId - The user ID to remove. + */ removeUser(args: RemoveChannelUserArgs): Promise { return this.simple('POST', 'remove_user', { ...args }, ChannelSchema) } + /** + * Removes multiple users from a channel. + * + * @param args - The arguments for removing users. + * @param args.id - The channel ID. + * @param args.userIds - Array of user IDs to remove. + */ removeUsers(args: RemoveChannelUsersArgs): Promise { return this.simple('POST', 'remove_users', { ...args }, ChannelSchema) } diff --git a/src/clients/comments-client.ts b/src/clients/comments-client.ts index 5dcac4d..40a92cf 100644 --- a/src/clients/comments-client.ts +++ b/src/clients/comments-client.ts @@ -19,9 +19,26 @@ export const CommentListSchema = z.array(CommentSchema) */ export class CommentsClient extends BaseClient { /** - * Lists comments in a thread. `newerThan` / `olderThan` (`Date`) are + * Gets all comments for a thread. `newerThan` / `olderThan` (`Date`) are * converted to `newer_than_ts` / `older_than_ts` epoch seconds on the * wire. + * + * @param args - The arguments for getting comments. + * @param args.threadId - The thread ID. + * @param args.from - @deprecated Use `newerThan` instead. + * @param args.newerThan - Optional date to get comments newer than. + * @param args.olderThan - Optional date to get comments older than. + * @param args.limit - Optional limit on number of comments returned. + * @returns An array of comment objects. + * + * @example + * ```typescript + * const comments = await api.comments.getComments({ + * threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z', + * newerThan: new Date('2024-01-01'), + * }) + * comments.forEach(c => console.log(c.content)) + * ``` */ getComments(args: GetCommentsArgs): Promise { const params: Record = { threadId: args.threadId } @@ -40,7 +57,12 @@ export class CommentsClient extends BaseClient { }).then((response) => CommentListSchema.parse(response.data)) } - /** Fetches a single comment by ID. The API wraps it in `{comment: ...}`. */ + /** + * Gets a single comment object by id. The API wraps it in `{comment: ...}`. + * + * @param id - The comment ID. + * @returns The comment object. + */ getComment(id: string): Promise { const wrappedSchema = z.object({ comment: CommentSchema }).transform((data) => data.comment) return request({ @@ -54,7 +76,32 @@ export class CommentsClient extends BaseClient { } /** - * Creates a new comment. `id` is auto-generated if not supplied. + * Creates a new comment on a thread. `id` is auto-generated if not supplied. + * + * @param args - The arguments for creating a comment. + * @param args.threadId - The thread ID. + * @param args.content - The comment content. + * @param args.recipients - Optional array of user IDs to notify directly. + * @param args.groups - Optional array of custom group IDs to notify. + * @param args.directMentions - Optional array of user IDs that were @-mentioned in + * `content`. + * @param args.notifyAudience - Optional broader audience to notify in addition to + * `recipients` and `groups`. `'channel'` notifies everyone in the channel; + * `'thread'` notifies everyone who has interacted with the thread. + * @param args.attachments - Optional array of {@link Attachment} objects. + * @param args.sendAsIntegration - Optional flag to send as integration. + * @returns The created comment object. + * + * @example + * ```typescript + * // Notify everyone who has interacted with the thread, plus two extra users. + * const comment = await api.comments.createComment({ + * threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z', + * content: 'Great idea! Let\'s proceed.', + * notifyAudience: 'thread', + * recipients: [101, 202], + * }) + * ``` */ createComment(args: CreateCommentArgs): Promise { return addCommentRequest( @@ -63,7 +110,15 @@ export class CommentsClient extends BaseClient { ) } - /** Updates a comment. */ + /** + * Updates a comment's properties. + * + * @param args - The arguments for updating a comment. + * @param args.id - The comment ID. + * @param args.content - Optional new comment content. + * @param args.recipients - Optional array of user IDs to notify. + * @returns The updated comment object. + */ updateComment(args: UpdateCommentArgs): Promise { return request({ httpMethod: 'POST', @@ -75,7 +130,11 @@ export class CommentsClient extends BaseClient { }).then((response) => CommentSchema.parse(response.data)) } - /** Permanently deletes a comment. */ + /** + * Permanently deletes a comment. + * + * @param id - The comment ID. + */ deleteComment(id: string): Promise { return request({ httpMethod: 'POST', @@ -88,7 +147,18 @@ export class CommentsClient extends BaseClient { } /** - * Marks the user's read position in a thread. Comment IDs are strings. + * Marks the user's read position in a thread. Used to track where the user has read up to, + * so clients can scroll to this position and show a visual indicator (blue line). + * Comment IDs are strings. + * + * @param args - The arguments for marking read position. + * @param args.threadId - The thread ID. + * @param args.commentId - The comment ID to mark as the last read position. + * + * @example + * ```typescript + * await api.comments.markPosition({ threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z', commentId: '7YpL3oZ4kZ9vP7Q1tR2sX41' }) + * ``` */ markPosition(args: MarkCommentPositionArgs): Promise { return request({ diff --git a/src/clients/conversation-messages-client.ts b/src/clients/conversation-messages-client.ts index 0dcc6bc..2b10d7d 100644 --- a/src/clients/conversation-messages-client.ts +++ b/src/clients/conversation-messages-client.ts @@ -22,7 +22,25 @@ export const ConversationMessageListSchema = z.array(ConversationMessageSchema) * message `id` on `createMessage` when the caller doesn't supply one. */ export class ConversationMessagesClient extends BaseClient { - /** Lists messages in a conversation. */ + /** + * Gets all messages in a conversation. + * + * @param args - The arguments for getting messages. + * @param args.conversationId - The conversation ID. + * @param args.newerThan - Optional date to get messages newer than. + * @param args.olderThan - Optional date to get messages older than. + * @param args.limit - Optional limit on number of messages returned. + * @param args.cursor - Optional cursor for pagination. + * @returns An array of message objects. + * + * @example + * ```typescript + * const messages = await api.conversationMessages.getMessages({ + * conversationId: '7YpL3oZ4kZ9vP7Q1tR2sX42', + * newerThan: new Date('2024-01-01'), + * }) + * ``` + */ getMessages(args: GetConversationMessagesArgs): Promise { const params: Record = { conversationId: args.conversationId } if (args.newerThan) params.newerThanTs = Math.floor(args.newerThan.getTime() / 1000) @@ -40,12 +58,40 @@ export class ConversationMessagesClient extends BaseClient { }).then((response) => ConversationMessageListSchema.parse(response.data)) } - /** Fetches a single message by ID. */ + /** + * Gets a single conversation message by id. + * + * @param id - The message ID. + * @returns The conversation message object. + * + * @example + * ```typescript + * const message = await api.conversationMessages.getMessage('7YpL3oZ4kZ9vP7Q1tR2sX43') + * ``` + */ getMessage(id: string): Promise { return this.simple('GET', 'getone', { id }, ConversationMessageSchema) } - /** Creates a new message. `id` is auto-generated if not supplied. */ + /** + * Creates a new message in a conversation. `id` is auto-generated if not + * supplied. + * + * @param args - The arguments for creating a message. + * @param args.conversationId - The conversation ID. + * @param args.content - The message content. + * @param args.attachments - Optional array of {@link Attachment} objects. + * @param args.actions - Optional array of action objects. + * @returns The created message object. + * + * @example + * ```typescript + * const message = await api.conversationMessages.createMessage({ + * conversationId: '7YpL3oZ4kZ9vP7Q1tR2sX42', + * content: 'Thanks for the update!', + * }) + * ``` + */ createMessage(args: CreateConversationMessageArgs): Promise { const params: Record = { conversationId: args.conversationId, @@ -61,7 +107,23 @@ export class ConversationMessagesClient extends BaseClient { return this.simple('POST', 'add', params, ConversationMessageSchema) } - /** Updates a message. */ + /** + * Updates a conversation message. + * + * @param args - The arguments for updating a message. + * @param args.id - The message ID. + * @param args.content - The new message content. + * @param args.attachments - Optional array of {@link Attachment} objects. + * @returns The updated message object. + * + * @example + * ```typescript + * const message = await api.conversationMessages.updateMessage({ + * id: '7YpL3oZ4kZ9vP7Q1tR2sX43', + * content: 'Updated message content', + * }) + * ``` + */ updateMessage(args: UpdateConversationMessageArgs): Promise { const params: Record = { id: args.id, content: args.content } if (args.attachments) params.attachments = args.attachments @@ -72,7 +134,16 @@ export class ConversationMessagesClient extends BaseClient { return this.simple('POST', 'update', params, ConversationMessageSchema) } - /** Permanently deletes a message. */ + /** + * Permanently deletes a conversation message. + * + * @param id - The message ID. + * + * @example + * ```typescript + * await api.conversationMessages.deleteMessage('7YpL3oZ4kZ9vP7Q1tR2sX43') + * ``` + */ deleteMessage(id: string): Promise { return this.simple('POST', 'remove', { id }, StatusOkSchema) } diff --git a/src/clients/conversations-client.ts b/src/clients/conversations-client.ts index f505921..6166bfa 100644 --- a/src/clients/conversations-client.ts +++ b/src/clients/conversations-client.ts @@ -36,7 +36,20 @@ const GetUnreadResponseSchema = z.object({ * already-assigned `id` and your generated one is silently dropped. */ export class ConversationsClient extends BaseClient { - /** Lists conversations in a workspace. */ + /** + * Gets all conversations for a workspace. + * + * @param args - The arguments for getting conversations. + * @param args.workspaceId - The workspace ID. + * @param args.archived - Optional flag to include archived conversations. + * @returns An array of conversation objects. + * + * @example + * ```typescript + * const conversations = await api.conversations.getConversations({ workspaceId: 123 }) + * conversations.forEach(c => console.log(c.title)) + * ``` + */ getConversations(args: GetConversationsArgs): Promise { return request({ httpMethod: 'GET', @@ -48,7 +61,12 @@ export class ConversationsClient extends BaseClient { }).then((response) => ConversationListSchema.parse(response.data)) } - /** Fetches a single conversation by ID. */ + /** + * Gets a single conversation object by id. + * + * @param id - The conversation ID. + * @returns The conversation object. + */ getConversation(id: string): Promise { return this.simple('GET', 'getone', { id }, ConversationSchema) } @@ -57,6 +75,19 @@ export class ConversationsClient extends BaseClient { * Gets an existing 1:1 / group conversation with `userIds`, or creates a * new one. `id` is auto-generated if not supplied — on dedupe, the * backend returns the existing conversation's `id` instead. + * + * @param args - The arguments for getting or creating a conversation. + * @param args.workspaceId - The workspace ID. + * @param args.userIds - Array of user IDs to include in the conversation. + * @returns The conversation object (existing or newly created). + * + * @example + * ```typescript + * const conversation = await api.conversations.getOrCreateConversation({ + * workspaceId: 123, + * userIds: [101, 202, 303], + * }) + * ``` */ getOrCreateConversation(args: GetOrCreateConversationArgs): Promise { return this.simple( @@ -67,41 +98,122 @@ export class ConversationsClient extends BaseClient { ) } - /** Updates a conversation's title. */ + /** + * Updates a conversation's title. + * + * @param args - The arguments for updating a conversation. + * @param args.id - The conversation ID. + * @param args.title - The new title for the conversation. + * @param args.archived - Optional flag to archive/unarchive the conversation. + * @returns The updated conversation object. + * + * @example + * ```typescript + * const conversation = await api.conversations.updateConversation({ + * id: '7YpL3oZ4kZ9vP7Q1tR2sX42', + * title: 'New Title', + * }) + * ``` + */ updateConversation(args: UpdateConversationArgs): Promise { const params: Record = { id: args.id, title: args.title } if (args.archived !== undefined) params.archived = args.archived return this.simple('POST', 'update', params, ConversationSchema) } + /** + * Archives a conversation. + * + * @param id - The conversation ID. + * @returns The updated conversation object. + */ archiveConversation(id: string): Promise { return this.simple('GET', 'archive', { id }, ConversationSchema) } + /** + * Unarchives a conversation. + * + * @param id - The conversation ID. + * @returns The updated conversation object. + */ unarchiveConversation(id: string): Promise { return this.simple('GET', 'unarchive', { id }, ConversationSchema) } + /** + * Adds a user to a conversation. + * + * @param args - The arguments for adding a user. + * @param args.id - The conversation ID. + * @param args.userId - The user ID to add. + * @returns The updated conversation object. + */ addUser(args: AddConversationUserArgs): Promise { return this.simple('POST', 'add_user', { ...args }, ConversationSchema) } + /** + * Adds multiple users to a conversation. + * + * @param args - The arguments for adding users. + * @param args.id - The conversation ID. + * @param args.userIds - Array of user IDs to add. + * @returns The updated conversation object. + * + * @example + * ```typescript + * await api.conversations.addUsers({ id: '7YpL3oZ4kZ9vP7Q1tR2sX42', userIds: [101, 202] }) + * ``` + */ addUsers(args: AddConversationUsersArgs): Promise { return this.simple('POST', 'add_users', { ...args }, ConversationSchema) } + /** + * Removes a user from a conversation. + * + * @param args - The arguments for removing a user. + * @param args.id - The conversation ID. + * @param args.userId - The user ID to remove. + * @returns The updated conversation object. + */ removeUser(args: RemoveConversationUserArgs): Promise { return this.simple('POST', 'remove_user', { ...args }, ConversationSchema) } + /** + * Removes multiple users from a conversation. + * + * @param args - The arguments for removing users. + * @param args.id - The conversation ID. + * @param args.userIds - Array of user IDs to remove. + * @returns The updated conversation object. + */ removeUsers(args: RemoveConversationUsersArgs): Promise { return this.simple('POST', 'remove_users', { ...args }, ConversationSchema) } + /** + * Marks a conversation as read. + * + * @param args - The arguments for marking as read. + * @param args.id - The conversation ID. + * @param args.objIndex - Optional index of the message to mark as last read. + * @param args.messageId - Optional message ID to mark as last read. + */ markRead(args: { id: string; objIndex?: number; messageId?: string }): Promise { return this.simple('POST', 'mark_read', { ...args }, StatusOkSchema) } + /** + * Marks a conversation as unread. + * + * @param args - The arguments for marking as unread. + * @param args.id - The conversation ID. + * @param args.objIndex - Optional index of the message to mark as last unread. + * @param args.messageId - Optional message ID to mark as last unread. + */ markUnread(args: { id: string; objIndex?: number; messageId?: string }): Promise { return this.simple('POST', 'mark_unread', { ...args }, StatusOkSchema) } @@ -109,19 +221,47 @@ export class ConversationsClient extends BaseClient { /** * Returns unread conversations for a workspace, paired with the unread * version counter. + * + * @param workspaceId - The workspace ID. + * @returns Object containing the array of unread conversation references and a version counter. */ getUnread(workspaceId: number): Promise<{ data: UnreadConversation[]; version: number }> { return this.simple('GET', 'get_unread', { workspaceId }, GetUnreadResponseSchema) } + /** + * Clears all unread conversations for a workspace. + * + * @param workspaceId - The workspace ID. + */ clearUnread(workspaceId: number): Promise { return this.simple('GET', 'clear_unread', { workspaceId }, StatusOkSchema) } + /** + * Mutes a conversation for a specified number of minutes. + * The user will receive no notifications from this conversation during that period. + * + * @param args - The arguments for muting a conversation. + * @param args.id - The conversation ID. + * @param args.minutes - Number of minutes to mute the conversation. + * @returns The updated conversation object. + * + * @example + * ```typescript + * const conversation = await api.conversations.muteConversation({ id: '7YpL3oZ4kZ9vP7Q1tR2sX42', minutes: 30 }) + * ``` + */ muteConversation(args: MuteConversationArgs): Promise { return this.simple('GET', 'mute', { ...args }, ConversationSchema) } + /** + * Unmutes a conversation. + * + * @param id - The conversation ID. + * @returns The updated conversation object. + */ unmuteConversation(id: string): Promise { return this.simple('GET', 'unmute', { id }, ConversationSchema) } diff --git a/src/clients/groups-client.ts b/src/clients/groups-client.ts index 970fe32..dbcf4ef 100644 --- a/src/clients/groups-client.ts +++ b/src/clients/groups-client.ts @@ -23,7 +23,18 @@ export const GroupListSchema = z.array(GroupSchema) * `workspace_id` alongside the group `id`. */ export class GroupsClient extends BaseClient { - /** Lists groups in a workspace. */ + /** + * Gets all groups for a given workspace. + * + * @param workspaceId - The workspace ID. + * @returns An array of group objects. + * + * @example + * ```typescript + * const groups = await api.groups.getGroups(123) + * groups.forEach(g => console.log(g.name)) + * ``` + */ getGroups(workspaceId: number): Promise { return request({ httpMethod: 'GET', @@ -35,12 +46,37 @@ export class GroupsClient extends BaseClient { }).then((response) => GroupListSchema.parse(response.data)) } - /** Fetches a single group by ID (requires `workspaceId`). */ + /** + * Gets a single group object by id. Requires `workspaceId`. + * + * @param args.id - The group ID. + * @param args.workspaceId - The workspace ID. + * @returns The group object. + */ getGroup(args: { id: string; workspaceId: number }): Promise { return this.simple('GET', 'getone', { ...args }, GroupSchema) } - /** Creates a new group. `id` is auto-generated if not supplied. */ + /** + * Creates a new group. `id` is auto-generated if not supplied. + * + * @param args - The arguments for creating a group. + * @param args.workspaceId - The workspace ID. + * @param args.name - The group name. + * @param args.id - Optional caller-supplied group ID (for optimistic-UI workflows). + * @param args.description - Optional group description. + * @param args.userIds - Optional array of user IDs to add to the group. + * @returns The created group object. + * + * @example + * ```typescript + * const group = await api.groups.createGroup({ + * workspaceId: 123, + * name: 'Engineering Team', + * userIds: [1, 2, 3], + * }) + * ``` + */ createGroup(args: { workspaceId: number name: string @@ -51,7 +87,16 @@ export class GroupsClient extends BaseClient { return this.simple('POST', 'add', { ...args, id: resolveCreateId(args.id) }, GroupSchema) } - /** Updates a group. Requires `workspaceId`. */ + /** + * Updates a group's properties. Requires `workspaceId`. + * + * @param args - The arguments for updating a group. + * @param args.id - The group ID. + * @param args.workspaceId - The workspace ID. + * @param args.name - Optional new group name. + * @param args.description - Optional new group description. + * @returns The updated group object. + */ updateGroup(args: { id: string workspaceId: number @@ -61,23 +106,61 @@ export class GroupsClient extends BaseClient { return this.simple('POST', 'update', { ...args }, GroupSchema) } - /** Permanently deletes a group. Requires `workspaceId`. */ + /** + * Permanently deletes a group. Requires `workspaceId`. + * + * @param args.id - The group ID. + * @param args.workspaceId - The workspace ID. + */ deleteGroup(args: { id: string; workspaceId: number }): Promise { return this.simple('POST', 'remove', { ...args }, StatusOkSchema) } + /** + * Adds a user to a group. + * + * @param args - The arguments for adding a user. + * @param args.id - The group ID. + * @param args.userId - The user ID to add. + */ addUser(args: AddGroupUserArgs): Promise { return this.simple('POST', 'add_user', { ...args }, StatusOkSchema) } + /** + * Adds multiple users to a group. + * + * @param args - The arguments for adding users. + * @param args.id - The group ID. + * @param args.userIds - Array of user IDs to add. + * + * @example + * ```typescript + * await api.groups.addUsers({ id: '7YpL3oZ4kZ9vP7Q1tR2sX45', userIds: [101, 202, 303] }) + * ``` + */ addUsers(args: AddGroupUsersArgs): Promise { return this.simple('POST', 'add_users', { ...args }, StatusOkSchema) } + /** + * Removes a user from a group. + * + * @param args - The arguments for removing a user. + * @param args.id - The group ID. + * @param args.userId - The user ID to remove. + */ removeUser(args: RemoveGroupUserArgs): Promise { return this.simple('POST', 'remove_user', { ...args }, StatusOkSchema) } + /** + * Removes multiple users from a group. + * + * @param args - The arguments for removing users. + * @param args.id - The group ID. + * @param args.userIds - Array of user IDs to remove. + */ removeUsers(args: RemoveGroupUsersArgs): Promise { return this.simple('POST', 'remove_users', { ...args }, StatusOkSchema) } diff --git a/src/clients/inbox-client.ts b/src/clients/inbox-client.ts index 7cd3c9e..3f96ecd 100644 --- a/src/clients/inbox-client.ts +++ b/src/clients/inbox-client.ts @@ -13,6 +13,31 @@ type InboxCountResponse = { export class InboxClient extends BaseClient { /** * Gets inbox items (threads). + * + * @param args - The arguments for getting inbox. + * @param args.workspaceId - The workspace ID. + * @param args.newerThan - Optional date to get items newer than. + * @param args.olderThan - Optional date to get items older than. + * @param args.since - @deprecated Use `newerThan` instead. + * @param args.until - @deprecated Use `olderThan` instead. + * @param args.limit - Optional limit on number of items returned. + * @param args.cursor - Optional cursor for pagination. + * @param args.archiveFilter - Optional filter: 'active' (default), 'archived', or 'all'. + * @returns Inbox threads. + * + * @example + * ```typescript + * const inbox = await api.inbox.getInbox({ + * workspaceId: 123, + * newerThan: new Date('2024-01-01'), + * }) + * + * // Include archived (done) items alongside active ones + * const allInbox = await api.inbox.getInbox({ + * workspaceId: 123, + * archiveFilter: 'all', + * }) + * ``` */ getInbox(args: GetInboxArgs): Promise { const params: Record = { workspace_id: args.workspaceId } @@ -34,7 +59,18 @@ export class InboxClient extends BaseClient { }).then((response) => response.data.map((thread) => InboxThreadSchema.parse(thread))) } - /** Gets unread count for inbox. */ + /** + * Gets unread count for inbox. + * + * @param workspaceId - The workspace ID. + * @returns The unread count. + * + * @example + * ```typescript + * const count = await api.inbox.getCount(123) + * console.log(`Unread items: ${count}`) + * ``` + */ getCount(workspaceId: number): Promise { return request({ httpMethod: 'GET', @@ -46,7 +82,16 @@ export class InboxClient extends BaseClient { }).then((response) => response.data.data) } - /** Archives a thread in the inbox. */ + /** + * Archives a thread in the inbox. + * + * @param id - The thread ID. + * + * @example + * ```typescript + * await api.inbox.archiveThread('7YpL3oZ4kZ9vP7Q1tR2sX3z') + * ``` + */ archiveThread(id: string): Promise { return request({ httpMethod: 'POST', @@ -58,7 +103,16 @@ export class InboxClient extends BaseClient { }).then(() => undefined) } - /** Unarchives a thread in the inbox. */ + /** + * Unarchives a thread in the inbox. + * + * @param id - The thread ID. + * + * @example + * ```typescript + * await api.inbox.unarchiveThread('7YpL3oZ4kZ9vP7Q1tR2sX3z') + * ``` + */ unarchiveThread(id: string): Promise { return request({ httpMethod: 'POST', @@ -70,7 +124,16 @@ export class InboxClient extends BaseClient { }).then(() => undefined) } - /** Marks all inbox items as read in a workspace. */ + /** + * Marks all inbox items as read in a workspace. + * + * @param workspaceId - The workspace ID. + * + * @example + * ```typescript + * await api.inbox.markAllRead(123) + * ``` + */ markAllRead(workspaceId: number): Promise { return request({ httpMethod: 'POST', @@ -82,7 +145,24 @@ export class InboxClient extends BaseClient { }).then(() => undefined) } - /** Archives all inbox items in a workspace. */ + /** + * Archives all inbox items in a workspace. + * + * @param args - The arguments for archiving all. + * @param args.workspaceId - The workspace ID. + * @param args.channelIds - Optional array of channel IDs to filter by. + * @param args.olderThan - Optional date to filter items older than. + * @param args.until - @deprecated Use `olderThan` instead. + * @param args.since - @deprecated Not supported by the archive_all endpoint — this value is ignored. + * + * @example + * ```typescript + * await api.inbox.archiveAll({ + * workspaceId: 123, + * olderThan: new Date('2024-01-01'), + * }) + * ``` + */ archiveAll(args: ArchiveAllArgs): Promise { const params: Record = { workspace_id: args.workspaceId } if (args.channelIds) params.channel_ids = args.channelIds diff --git a/src/clients/reactions-client.ts b/src/clients/reactions-client.ts index da473ab..87b659f 100644 --- a/src/clients/reactions-client.ts +++ b/src/clients/reactions-client.ts @@ -21,7 +21,20 @@ function reactionTarget(args: { * Client for interacting with Comms reaction endpoints. */ export class ReactionsClient extends BaseClient { - /** Adds an emoji reaction to a thread, comment, or conversation message. */ + /** + * Adds an emoji reaction to a thread, comment, or conversation message. + * + * @param args - The arguments for adding a reaction. + * @param args.threadId - Optional thread ID. + * @param args.commentId - Optional comment ID. + * @param args.messageId - Optional message ID (for conversation messages). + * @param args.reaction - The reaction emoji to add. + * + * @example + * ```typescript + * await api.reactions.add({ threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z', reaction: '👍' }) + * ``` + */ add(args: AddReactionArgs): Promise { return request({ httpMethod: 'POST', @@ -38,6 +51,18 @@ export class ReactionsClient extends BaseClient { * * Returns an object with emoji reactions as keys and arrays of user IDs as * values, or null if no reactions. + * + * @param args - The arguments for getting reactions. + * @param args.threadId - Optional thread ID. + * @param args.commentId - Optional comment ID. + * @param args.messageId - Optional message ID (for conversation messages). + * @returns A reaction object with emoji reactions as keys and arrays of user IDs as values, or null if no reactions. + * + * @example + * ```typescript + * const reactions = await api.reactions.get({ threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z' }) + * // Returns: { "👍": [101, 202, 303], "❤️": [101, 202] } + * ``` */ get(args: GetReactionsArgs): Promise { return request({ @@ -50,7 +75,20 @@ export class ReactionsClient extends BaseClient { }).then((response) => response.data) } - /** Removes an emoji reaction from a thread, comment, or conversation message. */ + /** + * Removes an emoji reaction from a thread, comment, or conversation message. + * + * @param args - The arguments for removing a reaction. + * @param args.threadId - Optional thread ID. + * @param args.commentId - Optional comment ID. + * @param args.messageId - Optional message ID (for conversation messages). + * @param args.reaction - The reaction emoji to remove. + * + * @example + * ```typescript + * await api.reactions.remove({ threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z', reaction: '👍' }) + * ``` + */ remove(args: RemoveReactionArgs): Promise { return request({ httpMethod: 'POST', diff --git a/src/clients/search-client.ts b/src/clients/search-client.ts index ebb9f11..3c9650f 100644 --- a/src/clients/search-client.ts +++ b/src/clients/search-client.ts @@ -15,6 +15,26 @@ import { BaseClient } from './base-client' export class SearchClient extends BaseClient { /** * Searches across all threads and conversations in a workspace. + * + * @param args - The arguments for searching. + * @param args.query - The search query string. Optional when `mentionSelf: true` is set; required otherwise. + * @param args.workspaceId - The workspace ID to search in. + * @param args.channelIds - Optional array of channel IDs to filter by. + * @param args.authorIds - Optional array of author user IDs to filter by. + * @param args.mentionSelf - Optional flag to filter by mentions of the current user. When true, `query` may be omitted. + * @param args.dateFrom - Optional start date for filtering (YYYY-MM-DD). + * @param args.dateTo - Optional end date for filtering (YYYY-MM-DD). + * @param args.limit - Optional limit on number of results returned. + * @param args.cursor - Optional cursor for pagination. + * @returns Search results with pagination. + * + * @example + * ```typescript + * const results = await api.search.search({ + * query: 'important meeting', + * workspaceId: 123, + * }) + * ``` */ search(args: SearchArgs): Promise { const params: Record = { workspace_id: args.workspaceId } @@ -42,6 +62,21 @@ export class SearchClient extends BaseClient { /** * Searches within comments of a specific thread. + * + * @param args - The arguments for searching within a thread. + * @param args.query - The search query string. + * @param args.threadId - The thread ID to search in. + * @param args.limit - Optional limit on number of results returned. + * @param args.cursor - Optional cursor for pagination. + * @returns Comment IDs that match the search query. + * + * @example + * ```typescript + * const results = await api.search.searchThread({ + * query: 'deadline', + * threadId: '7YpL3oZ4kZ9vP7Q1tR2sX3z', + * }) + * ``` */ searchThread(args: SearchThreadArgs): Promise { const params: Record = { @@ -63,6 +98,21 @@ export class SearchClient extends BaseClient { /** * Searches within messages of a specific conversation. + * + * @param args - The arguments for searching within a conversation. + * @param args.query - The search query string. + * @param args.conversationId - The conversation ID to search in. + * @param args.limit - Optional limit on number of results returned. + * @param args.cursor - Optional cursor for pagination. + * @returns Message IDs that match the search query. + * + * @example + * ```typescript + * const results = await api.search.searchConversation({ + * query: 'budget', + * conversationId: '7YpL3oZ4kZ9vP7Q1tR2sX42', + * }) + * ``` */ searchConversation(args: SearchConversationArgs): Promise { const params: Record = { diff --git a/src/clients/threads-client.ts b/src/clients/threads-client.ts index 82bec96..799750e 100644 --- a/src/clients/threads-client.ts +++ b/src/clients/threads-client.ts @@ -41,9 +41,26 @@ const GetUnreadResponseSchema = z.object({ */ export class ThreadsClient extends BaseClient { /** - * Lists threads. At least one of `channelId` / `workspaceId` is required. + * Gets threads. At least one of `channelId` / `workspaceId` is required. * `newerThan` / `olderThan` (`Date`) are converted to the * `newer_than_ts` / `older_than_ts` epoch-second params on the wire. + * + * @param args - The arguments for getting threads. + * @param args.channelId - The channel ID. + * @param args.workspaceId - Optional workspace ID. + * @param args.archived - Optional flag to include archived threads. + * @param args.newerThan - Optional date to get threads newer than. + * @param args.olderThan - Optional date to get threads older than. + * @param args.newer_than_ts - @deprecated Use `newerThan` instead. + * @param args.older_than_ts - @deprecated Use `olderThan` instead. + * @param args.limit - Optional limit on number of threads returned. + * @returns An array of thread objects. + * + * @example + * ```typescript + * const threads = await api.threads.getThreads({ channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44' }) + * threads.forEach(t => console.log(t.title)) + * ``` */ getThreads(args: GetThreadsArgs): Promise { const { newerThan, olderThan, newer_than_ts, older_than_ts, ...rest } = args @@ -65,57 +82,140 @@ export class ThreadsClient extends BaseClient { }).then((response) => ThreadListSchema.parse(response.data)) } - /** Fetches a single thread by ID. */ + /** + * Gets a single thread object by id. + * + * @param id - The thread ID. + * @returns The thread object. + */ getThread(id: string): Promise { return this.simple('GET', 'getone', { id }, ThreadSchema) } - /** Creates a new thread. `id` is auto-generated if not supplied. */ + /** + * Creates a new thread in a channel. `id` is auto-generated if not supplied. + * + * @param args - The arguments for creating a thread. + * @param args.channelId - The channel ID. + * @param args.title - The thread title. + * @param args.content - The thread content. + * @param args.recipients - Optional array of user IDs to notify. + * @param args.sendAsIntegration - Optional flag to send as integration. + * @returns The created thread object. + * + * @example + * ```typescript + * const thread = await api.threads.createThread({ + * channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44', + * title: 'New Feature Discussion', + * content: 'Let\'s discuss the new feature...', + * }) + * ``` + */ createThread(args: CreateThreadArgs): Promise { return this.simple('POST', 'add', { ...args, id: resolveCreateId(args.id) }, ThreadSchema) } - /** Partial update of an existing thread. */ + /** + * Partial update of an existing thread. + * + * @param args - The arguments for updating a thread. + * @param args.id - The thread ID. + * @param args.title - Optional new thread title. + * @param args.content - Optional new thread content. + * @param args.recipients - Optional array of user IDs to notify. + * @returns The updated thread object. + */ updateThread(args: UpdateThreadArgs): Promise { return this.simple('POST', 'update', { ...args }, ThreadSchema) } - /** Permanently deletes a thread. */ + /** + * Permanently deletes a thread. + * + * @param id - The thread ID. + */ deleteThread(id: string): Promise { return this.simple('POST', 'remove', { id }, StatusOkSchema) } - /** Saves a thread (formerly "star"). */ + /** + * Saves a thread (formerly "star"). + * + * @param id - The thread ID. + */ saveThread(id: string): Promise { return this.simple('GET', 'save', { id }, StatusOkSchema) } - /** Unsaves a thread (formerly "unstar"). */ + /** + * Unsaves a thread (formerly "unstar"). + * + * @param id - The thread ID. + */ unsaveThread(id: string): Promise { return this.simple('GET', 'unsave', { id }, StatusOkSchema) } + /** + * Pins a thread. + * + * @param id - The thread ID. + */ pinThread(id: string): Promise { return this.simple('GET', 'pin', { id }, StatusOkSchema) } + /** + * Unpins a thread. + * + * @param id - The thread ID. + */ unpinThread(id: string): Promise { return this.simple('GET', 'unpin', { id }, StatusOkSchema) } - /** Moves a thread to another channel. */ + /** + * Moves a thread to another channel. + * + * @param args - The arguments for moving a thread. + * @param args.id - The thread ID. + * @param args.toChannel - The target channel ID. + * @returns The updated thread object. + */ moveToChannel(args: MoveThreadToChannelArgs): Promise { return this.simple('GET', 'move_to_channel', { ...args }, ThreadSchema) } + /** + * Marks a thread as read. + * + * @param args - The arguments for marking a thread as read. + * @param args.id - The thread ID. + * @param args.objIndex - The index of the last known read message. + */ markRead(args: MarkThreadReadArgs): Promise { return this.simple('POST', 'mark_read', { ...args }, StatusOkSchema) } + /** + * Marks a thread as unread. + * + * @param args - The arguments for marking a thread as unread. + * @param args.id - The thread ID. + * @param args.objIndex - The index of the last unread message. Use -1 to mark the whole thread as unread. + */ markUnread(args: MarkThreadUnreadArgs): Promise { return this.simple('POST', 'mark_unread', { ...args }, StatusOkSchema) } + /** + * Marks a thread as unread for others. Useful to notify others about thread changes. + * + * @param args - The arguments for marking a thread as unread for others. + * @param args.id - The thread ID. + * @param args.objIndex - The index of the last unread message. Use -1 to mark the whole thread as unread. + */ markUnreadForOthers(args: MarkThreadUnreadForOthersArgs): Promise { return this.simple('POST', 'mark_unread_for_others', { ...args }, StatusOkSchema) } @@ -123,6 +223,19 @@ export class ThreadsClient extends BaseClient { /** * Marks every thread in a workspace or channel as read. Exactly one of * `workspaceId` / `channelId` should be set. + * + * @param args - Either workspaceId or channelId (one is required). + * @param args.workspaceId - The workspace ID. + * @param args.channelId - The channel ID. + * + * @example + * ```typescript + * // Mark all in workspace + * await api.threads.markAllRead({ workspaceId: 123 }) + * + * // Mark all in channel + * await api.threads.markAllRead({ channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44' }) + * ``` */ markAllRead(args: { workspaceId?: number; channelId?: string }): Promise { if (!args.workspaceId && !args.channelId) { @@ -131,6 +244,11 @@ export class ThreadsClient extends BaseClient { return this.simple('POST', 'mark_all_read', { ...args }, StatusOkSchema) } + /** + * Clears unread threads in a workspace. + * + * @param workspaceId - The workspace ID. + */ clearUnread(workspaceId: number): Promise { return this.simple('GET', 'clear_unread', { workspaceId }, StatusOkSchema) } @@ -138,6 +256,9 @@ export class ThreadsClient extends BaseClient { /** * Returns unread threads for a workspace, paired with the unread version * counter and (optionally) the inbox unread count. + * + * @param workspaceId - The workspace ID. + * @returns Object containing the array of unread thread references, a version counter, and optionally the inbox unread count. */ getUnread(workspaceId: number): Promise<{ data: UnreadThread[] @@ -147,18 +268,89 @@ export class ThreadsClient extends BaseClient { return this.simple('GET', 'get_unread', { workspaceId }, GetUnreadResponseSchema) } + /** + * Mutes a thread for a specified number of minutes. + * When muted, you will not get notified in your inbox about new comments. + * + * @param args - The arguments for muting a thread. + * @param args.id - The thread ID. + * @param args.minutes - Number of minutes to mute the thread. + * @returns The updated thread object. + * + * @example + * ```typescript + * const thread = await api.threads.muteThread({ id: '7YpL3oZ4kZ9vP7Q1tR2sX3z', minutes: 30 }) + * ``` + */ muteThread(args: MuteThreadArgs): Promise { return this.simple('GET', 'mute', { ...args }, ThreadSchema) } + /** + * Unmutes a thread. + * You will start to see notifications in your inbox again when new comments are added. + * + * @param id - The thread ID. + * @returns The updated thread object. + */ unmuteThread(id: string): Promise { return this.simple('GET', 'unmute', { id }, ThreadSchema) } + /** + * Closes a thread by adding a comment with a close action. + * + * @param args - The arguments for closing a thread. + * @param args.id - The thread ID. + * @param args.content - The comment content. + * @param args.attachments - Optional array of {@link Attachment} objects. + * @param args.actions - Optional array of action objects. + * @param args.recipients - Optional array of user IDs to notify directly. + * @param args.groups - Optional array of custom group IDs to notify. + * @param args.directMentions - Optional array of user IDs that were @-mentioned in + * `content`. + * @param args.notifyAudience - Optional broader audience to notify in addition to + * `recipients` and `groups`. `'channel'` notifies everyone in the channel; + * `'thread'` notifies everyone who has interacted with the thread. + * @returns The created comment object. + * + * @example + * ```typescript + * const comment = await api.threads.closeThread({ + * id: '7YpL3oZ4kZ9vP7Q1tR2sX3z', + * content: 'Closing this thread — resolved.', + * }) + * ``` + */ closeThread(args: CloseThreadArgs): Promise { return this.addCommentWithAction(args, 'close') } + /** + * Reopens a thread by adding a comment with a reopen action. + * + * @param args - The arguments for reopening a thread. + * @param args.id - The thread ID. + * @param args.content - The comment content. + * @param args.attachments - Optional array of {@link Attachment} objects. + * @param args.actions - Optional array of action objects. + * @param args.recipients - Optional array of user IDs to notify directly. + * @param args.groups - Optional array of custom group IDs to notify. + * @param args.directMentions - Optional array of user IDs that were @-mentioned in + * `content`. + * @param args.notifyAudience - Optional broader audience to notify in addition to + * `recipients` and `groups`. `'channel'` notifies everyone in the channel; + * `'thread'` notifies everyone who has interacted with the thread. + * @returns The created comment object. + * + * @example + * ```typescript + * const comment = await api.threads.reopenThread({ + * id: '7YpL3oZ4kZ9vP7Q1tR2sX3z', + * content: 'Reopening — need further discussion.', + * }) + * ``` + */ reopenThread(args: ReopenThreadArgs): Promise { return this.addCommentWithAction(args, 'reopen') } diff --git a/src/clients/users-client.ts b/src/clients/users-client.ts index c645026..093d809 100644 --- a/src/clients/users-client.ts +++ b/src/clients/users-client.ts @@ -43,12 +43,38 @@ type MfaChallengeArgs = { * `loginWithTodoist` are the available entry points. */ export class UsersClient extends BaseClient { - /** Registers a new user via the Todoist-ID bridge. */ + /** + * Registers a new user via the Todoist-ID bridge. + * + * @param args - Registration arguments. + * @param args.name - The new user's full name. + * @param args.email - The new user's email. + * @param args.password - The new user's password. + * @param args.lang - Optional preferred language. + * @param args.acceptTerms - Optional flag confirming the user accepts the terms of service. + * @returns The newly registered user object. + */ register(args: RegisterArgs): Promise { return this.post(`${ENDPOINT_USERS}/register`, args, UserSchema, { authed: false }) } - /** Logs in an existing user. */ + /** + * Logs in an existing user. + * + * @param args - Login credentials. + * @param args.email - The user's email. + * @param args.password - The user's password. + * @param args.setSessionCookie - Optional flag to set a session cookie (default: true). + * @returns The authenticated user object. + * + * @example + * ```typescript + * const user = await api.users.login({ + * email: 'user@example.com', + * password: 'secret', + * }) + * ``` + */ login(args: LoginArgs): Promise { return this.post(`${ENDPOINT_USERS}/login`, args, UserSchema, { authed: false }) } @@ -56,6 +82,8 @@ export class UsersClient extends BaseClient { /** * Logs in using a valid token (sent via Authorization header). The SDK * client is already configured with the token, so no args are needed. + * + * @returns The authenticated user object. */ loginWithToken(): Promise { return this.post(`${ENDPOINT_USERS}/login_with_token`, undefined, UserSchema) @@ -65,12 +93,24 @@ export class UsersClient extends BaseClient { * Exchanges the browser's Todoist web-session cookie for a Comms session. * Only useful when running in a browser context on the shared Todoist * registrable domain — the cookie is sent automatically by the browser. + * + * @returns The authenticated user object. */ loginWithTodoist(): Promise { return this.post(`${ENDPOINT_USERS}/login_with_todoist`, {}, UserSchema, { authed: false }) } - /** Logs in (and auto-signs-up) via a Google ID token. */ + /** + * Logs in (and auto-signs-up) via a Google ID token. + * + * @param args - Google login arguments. + * @param args.idToken - The Google ID token. + * @param args.nonce - The nonce that was sent to Google. + * @param args.timezone - Optional user timezone. + * @param args.lang - Optional preferred language. + * @param args.mfaToken - Optional MFA token from a prior `mfaChallenge` response. + * @returns The authenticated user object. + */ loginWithGoogle(args: LoginWithGoogleArgs): Promise { return this.post(`${ENDPOINT_USERS}/login_with_google`, args, UserSchema, { authed: false }) } @@ -78,6 +118,12 @@ export class UsersClient extends BaseClient { /** * Completes an MFA challenge issued by `loginWithGoogle` (returns an MFA * token to pass back to `loginWithGoogle.mfaToken`). + * + * @param args - MFA challenge arguments. + * @param args.challengeId - The challenge ID from the prior login attempt. + * @param args.factor - The MFA factor identifier. + * @param args.methodType - The MFA method type. + * @returns The MFA token to forward to `loginWithGoogle`. */ mfaChallenge(args: MfaChallengeArgs): Promise { return request({ @@ -102,7 +148,17 @@ export class UsersClient extends BaseClient { }).then(() => undefined) } - /** Returns the user associated with the current access token. */ + /** + * Gets the user associated with the current access token. + * + * @returns The authenticated user's information. + * + * @example + * ```typescript + * const user = await api.users.getSessionUser() + * console.log(user.fullName, user.email) + * ``` + */ getSessionUser(): Promise { return this.get(`${ENDPOINT_USERS}/get_session_user`, undefined, UserSchema) } @@ -111,12 +167,23 @@ export class UsersClient extends BaseClient { * Fetches a single user. Defaults to the session user when no `id` is * passed. Cross-workspace lookups require that the caller and the target * share a workspace. + * + * @param args - Optional lookup arguments. + * @param args.id - The user ID. Defaults to the session user. + * @param args.workspaceId - Optional workspace ID for cross-workspace lookups. + * @param args.asList - Optional flag controlling list-style response. + * @returns The user object. */ getUser(args?: { id?: number; workspaceId?: number; asList?: boolean }): Promise { return this.get(`${ENDPOINT_USERS}/getone`, args ?? {}, UserSchema) } - /** Looks up a user by their email address. */ + /** + * Looks up a user by their email address. + * + * @param email - The email to look up. + * @returns The user object. + */ getUserByEmail(email: string): Promise { return this.get(`${ENDPOINT_USERS}/get_by_email`, { email }, UserSchema) } @@ -124,17 +191,39 @@ export class UsersClient extends BaseClient { /** * Updates the logged-in user's profile. Most fields are proxied to * Todoist (full name, password, language, timezone, etc.). + * + * @param args - The user properties to update. + * @returns The updated user object. + * + * @example + * ```typescript + * const user = await api.users.update({ + * fullName: 'John Doe', + * timezone: 'America/New_York', + * }) + * ``` */ update(args: UpdateUserArgs): Promise { return this.post(`${ENDPOINT_USERS}/update`, args, UserSchema) } - /** Updates the user's password. Requires `currentPassword`. */ + /** + * Updates the user's password. Requires `currentPassword`. + * + * @param args - Password update arguments. + * @param args.newPassword - The new password. + * @param args.currentPassword - The user's existing password (required to authenticate the change). + * @returns The updated user object. + */ updatePassword(args: { newPassword: string; currentPassword?: string }): Promise { return this.post(`${ENDPOINT_USERS}/update_password`, args, UserSchema) } - /** Removes the user's avatar. */ + /** + * Removes the user's avatar. + * + * @returns The updated user object. + */ removeAvatar(): Promise { return this.post(`${ENDPOINT_USERS}/remove_avatar`, undefined, UserSchema) } @@ -142,6 +231,14 @@ export class UsersClient extends BaseClient { /** * Invalidates the current API token and returns the user with a fresh * token. + * + * @returns The user object with the new token. + * + * @example + * ```typescript + * const user = await api.users.invalidateToken() + * console.log('New token:', user.token) + * ``` */ invalidateToken(): Promise { return this.post(`${ENDPOINT_USERS}/invalidate_token`, undefined, UserSchema) @@ -151,6 +248,8 @@ export class UsersClient extends BaseClient { * Validates that an arbitrary token is still active. Note this is sent * as a GET — the token is read from the query string, not the * Authorization header. + * + * @param token - The token to validate. */ validateToken(token: string): Promise { return request({ @@ -163,7 +262,18 @@ export class UsersClient extends BaseClient { }).then(() => undefined) } - /** Marks the user as active on a workspace (presence beacon). */ + /** + * Marks the user as active on a workspace (presence beacon). + * + * @param args - Heartbeat arguments. + * @param args.workspaceId - The workspace ID. + * @param args.platform - The platform identifier (e.g., 'mobile', 'desktop', 'api'). + * + * @example + * ```typescript + * await api.users.heartbeat({ workspaceId: 123, platform: 'api' }) + * ``` + */ heartbeat(args: { workspaceId: number; platform: string }): Promise { return request({ httpMethod: 'GET', @@ -175,7 +285,11 @@ export class UsersClient extends BaseClient { }).then(() => undefined) } - /** Resets the user's presence for a workspace. */ + /** + * Resets the user's presence for a workspace (marks the user as inactive). + * + * @param workspaceId - The workspace ID. + */ resetPresence(workspaceId: number): Promise { return request({ httpMethod: 'POST', @@ -187,7 +301,12 @@ export class UsersClient extends BaseClient { }).then(() => undefined) } - /** Checks whether an email address is registered (and verified). */ + /** + * Checks whether an email address is registered (and verified). + * + * @param email - The email to check. + * @returns Object indicating whether the email exists and is verified. + */ checkEmail(email: string): Promise { return request({ httpMethod: 'POST', @@ -202,6 +321,8 @@ export class UsersClient extends BaseClient { /** * Returns the current per-channel mail unsubscribe settings for the * caller's primary email. + * + * @returns Object mapping email-type keys to their opt-out flag. */ getUnsubscribeSettings(): Promise> { return request>({ @@ -214,7 +335,12 @@ export class UsersClient extends BaseClient { }).then((response) => response.data) } - /** Toggles per-email-type opt-out settings. */ + /** + * Toggles per-email-type opt-out settings. + * + * @param settings - Object mapping email-type keys to their opt-out flag. + * @returns Status object with `"ok"` status. + */ updateUnsubscribeSettings(settings: Record): Promise<{ status: string }> { return request<{ status: string }>({ httpMethod: 'POST', diff --git a/src/clients/workspace-users-client.ts b/src/clients/workspace-users-client.ts index 0539851..8b0c60e 100644 --- a/src/clients/workspace-users-client.ts +++ b/src/clients/workspace-users-client.ts @@ -15,7 +15,20 @@ import { BaseClient } from './base-client' * rejects non-empty `name` and `channelIds` — set neither. */ export class WorkspaceUsersClient extends BaseClient { - /** Returns workspace user objects for the given workspace id. */ + /** + * Returns a list of workspace user objects for the given workspace id. + * + * @param args - The arguments for getting workspace users. + * @param args.workspaceId - The workspace ID. + * @param args.archived - Optional flag to filter archived users. + * @returns An array of workspace user objects. + * + * @example + * ```typescript + * const users = await api.workspaceUsers.getWorkspaceUsers({ workspaceId: 123 }) + * users.forEach(u => console.log(u.fullName, u.userType)) + * ``` + */ getWorkspaceUsers(args: GetWorkspaceUsersArgs): Promise { return request({ httpMethod: 'GET', @@ -27,7 +40,12 @@ export class WorkspaceUsersClient extends BaseClient { }).then((response) => response.data.map((user) => WorkspaceUserSchema.parse(user))) } - /** Returns workspace user IDs for the given workspace id. */ + /** + * Returns a list of workspace user IDs for the given workspace id. + * + * @param workspaceId - The workspace ID. + * @returns An array of user IDs. + */ getWorkspaceUserIds(workspaceId: number): Promise { return request({ httpMethod: 'GET', @@ -39,7 +57,20 @@ export class WorkspaceUsersClient extends BaseClient { }).then((response) => response.data) } - /** Gets a user by id. */ + /** + * Gets a user by id. + * + * @param args - The arguments for getting a user by ID. + * @param args.workspaceId - The workspace ID. + * @param args.userId - The user's ID. + * @returns The workspace user object. + * + * @example + * ```typescript + * const user = await api.workspaceUsers.getUserById({ workspaceId: 123, userId: 456 }) + * console.log(user.fullName, user.email) + * ``` + */ getUserById(args: GetUserByIdArgs): Promise { return request({ httpMethod: 'GET', @@ -51,7 +82,22 @@ export class WorkspaceUsersClient extends BaseClient { }).then((response) => WorkspaceUserSchema.parse(response.data)) } - /** Gets a user by email. */ + /** + * Gets a user by email. + * + * @param args - The arguments for getting a user by email. + * @param args.workspaceId - The workspace ID. + * @param args.email - The user's email. + * @returns The workspace user object. + * + * @example + * ```typescript + * const user = await api.workspaceUsers.getUserByEmail({ + * workspaceId: 123, + * email: 'user@example.com', + * }) + * ``` + */ getUserByEmail(args: GetUserByEmailArgs): Promise { return request({ httpMethod: 'GET', @@ -63,7 +109,14 @@ export class WorkspaceUsersClient extends BaseClient { }).then((response) => WorkspaceUserSchema.parse(response.data)) } - /** Gets the user's info in the context of the workspace. */ + /** + * Gets the user's info in the context of the workspace. + * + * @param args - The arguments for getting user info. + * @param args.workspaceId - The workspace ID. + * @param args.userId - The user's ID. + * @returns Information about the user in the workspace context. + */ getUserInfo(args: GetUserInfoArgs): Promise> { return request>({ httpMethod: 'GET', @@ -75,7 +128,23 @@ export class WorkspaceUsersClient extends BaseClient { }).then((response) => response.data) } - /** Gets the user's local time (e.g., "2017-05-10 07:55:40"). */ + /** + * Gets the user's local time (e.g., "2017-05-10 07:55:40"). + * + * @param args - The arguments for getting user local time. + * @param args.workspaceId - The workspace ID. + * @param args.userId - The user's ID. + * @returns The user's local time as a string. + * + * @example + * ```typescript + * const localTime = await api.workspaceUsers.getUserLocalTime({ + * workspaceId: 123, + * userId: 456, + * }) + * console.log('User local time:', localTime) + * ``` + */ getUserLocalTime(args: GetUserLocalTimeArgs): Promise { return request({ httpMethod: 'GET', @@ -87,7 +156,15 @@ export class WorkspaceUsersClient extends BaseClient { }).then((response) => response.data) } - /** Adds a person to a workspace. */ + /** + * Adds a person to a workspace. + * + * @param args - The arguments for adding a user. + * @param args.workspaceId - The workspace ID. + * @param args.email - The user's email. + * @param args.userType - Optional user type (USER, GUEST, or ADMIN). + * @returns The created workspace user object. + */ addUser(args: { workspaceId: number email: string @@ -107,7 +184,16 @@ export class WorkspaceUsersClient extends BaseClient { }).then((response) => WorkspaceUserSchema.parse(response.data)) } - /** Updates a person in a workspace. */ + /** + * Updates a person in a workspace. + * + * @param args - The arguments for updating a user. + * @param args.workspaceId - The workspace ID. + * @param args.userType - The user type (USER, GUEST, or ADMIN). + * @param args.email - Optional email of the user to update. + * @param args.userId - Optional user ID to update (use either email or userId). + * @returns The updated workspace user object. + */ updateUser(args: { workspaceId: number userType: UserType @@ -129,7 +215,14 @@ export class WorkspaceUsersClient extends BaseClient { }).then((response) => WorkspaceUserSchema.parse(response.data)) } - /** Removes a person from a workspace. */ + /** + * Removes a person from a workspace. + * + * @param args - The arguments for removing a user. + * @param args.workspaceId - The workspace ID. + * @param args.email - Optional email of the user to remove. + * @param args.userId - Optional user ID to remove (use either email or userId). + */ removeUser(args: { workspaceId: number; email?: string; userId?: number }): Promise { return request({ httpMethod: 'POST', @@ -145,7 +238,14 @@ export class WorkspaceUsersClient extends BaseClient { }).then(() => undefined) } - /** Sends a new workspace invitation to the selected user. */ + /** + * Sends a new workspace invitation to the selected user. + * + * @param args - The arguments for resending an invite. + * @param args.workspaceId - The workspace ID. + * @param args.email - The user's email. + * @param args.userId - Optional user ID. + */ resendInvite(args: { workspaceId: number; email: string; userId?: number }): Promise { return request({ httpMethod: 'POST', diff --git a/src/clients/workspaces-client.ts b/src/clients/workspaces-client.ts index e8d0cd5..05810d1 100644 --- a/src/clients/workspaces-client.ts +++ b/src/clients/workspaces-client.ts @@ -11,7 +11,17 @@ export const ChannelListSchema = z.array(ChannelSchema) * currently rejects any `color` other than `1` on add/update. */ export class WorkspacesClient extends BaseClient { - /** Gets all the user's workspaces. */ + /** + * Gets all the user's workspaces. + * + * @returns An array of all workspaces the user belongs to. + * + * @example + * ```typescript + * const workspaces = await api.workspaces.getWorkspaces() + * workspaces.forEach(ws => console.log(ws.name)) + * ``` + */ getWorkspaces(): Promise { return request({ httpMethod: 'GET', @@ -23,7 +33,18 @@ export class WorkspacesClient extends BaseClient { }).then((response) => response.data.map((workspace) => WorkspaceSchema.parse(workspace))) } - /** Gets a single workspace object by id. */ + /** + * Gets a single workspace object by id. + * + * @param id - The workspace ID. + * @returns The workspace object. + * + * @example + * ```typescript + * const workspace = await api.workspaces.getWorkspace(123) + * console.log(workspace.name) + * ``` + */ getWorkspace(id: number): Promise { return request({ httpMethod: 'GET', @@ -35,7 +56,17 @@ export class WorkspacesClient extends BaseClient { }).then((response) => WorkspaceSchema.parse(response.data)) } - /** Gets the user's default workspace. */ + /** + * Gets the user's default workspace. + * + * @returns The default workspace object. + * + * @example + * ```typescript + * const workspace = await api.workspaces.getDefaultWorkspace() + * console.log(workspace.name) + * ``` + */ getDefaultWorkspace(): Promise { return request({ httpMethod: 'GET', @@ -47,7 +78,18 @@ export class WorkspacesClient extends BaseClient { }).then((response) => WorkspaceSchema.parse(response.data)) } - /** Creates a new workspace. */ + /** + * Creates a new workspace. + * + * @param name - The name of the new workspace. + * @returns The created workspace object. + * + * @example + * ```typescript + * const workspace = await api.workspaces.createWorkspace('My Team') + * console.log('Created:', workspace.name) + * ``` + */ createWorkspace(name: string): Promise { return request({ httpMethod: 'POST', @@ -59,7 +101,18 @@ export class WorkspacesClient extends BaseClient { }).then((response) => WorkspaceSchema.parse(response.data)) } - /** Updates an existing workspace. */ + /** + * Updates an existing workspace. + * + * @param id - The workspace ID. + * @param name - The new name for the workspace. + * @returns The updated workspace object. + * + * @example + * ```typescript + * const workspace = await api.workspaces.updateWorkspace(123, 'New Team Name') + * ``` + */ updateWorkspace(id: number, name: string): Promise { return request({ httpMethod: 'POST', @@ -71,7 +124,16 @@ export class WorkspacesClient extends BaseClient { }).then((response) => WorkspaceSchema.parse(response.data)) } - /** Removes a workspace and all its data (not recoverable). */ + /** + * Removes a workspace and all its data (not recoverable). + * + * @param id - The workspace ID. + * + * @example + * ```typescript + * await api.workspaces.removeWorkspace(123) + * ``` + */ removeWorkspace(id: number): Promise { return request({ httpMethod: 'POST', @@ -83,7 +145,18 @@ export class WorkspacesClient extends BaseClient { }).then(() => undefined) } - /** Gets the public channels of a workspace. */ + /** + * Gets the public channels of a workspace. + * + * @param id - The workspace ID. + * @returns An array of public channel objects. + * + * @example + * ```typescript + * const channels = await api.workspaces.getPublicChannels(123) + * channels.forEach(ch => console.log(ch.name)) + * ``` + */ getPublicChannels(id: number): Promise { return request({ httpMethod: 'GET', From 18a652d19ffeee4df5e5ea8b061a86ebc78072f7 Mon Sep 17 00:00:00 2001 From: Amir Date: Thu, 21 May 2026 13:01:56 +0200 Subject: [PATCH 3/5] refactor: extract test BASE URL into shared constant Each MSW test was redeclaring `const BASE = 'https://comms.todoist.com/api/v1'`. Moved to `TEST_API_BASE_URL` in `testUtils/test-defaults.ts`, imported as `BASE` at the call sites to keep the tests readable. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/clients/comments-client.test.ts | 9 ++++++--- src/clients/conversation-messages-client.test.ts | 8 +++++--- src/testUtils/test-defaults.ts | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/clients/comments-client.test.ts b/src/clients/comments-client.test.ts index 8728ae6..6540b6a 100644 --- a/src/clients/comments-client.test.ts +++ b/src/clients/comments-client.test.ts @@ -2,9 +2,12 @@ import { http, HttpResponse } from 'msw' import { describe, expect, it } from 'vitest' import { CommsApi } from '../comms-api' import { server } from '../testUtils/msw-setup' -import { TEST_API_TOKEN, TEST_COMMENT_ID, TEST_THREAD_ID } from '../testUtils/test-defaults' - -const BASE = 'https://comms.todoist.com/api/v1' +import { + TEST_API_BASE_URL as BASE, + TEST_API_TOKEN, + TEST_COMMENT_ID, + TEST_THREAD_ID, +} from '../testUtils/test-defaults' // These tests pin the wire shape of `comments-client` — every camelCase // field on the args side ends up snake_case on the wire (via diff --git a/src/clients/conversation-messages-client.test.ts b/src/clients/conversation-messages-client.test.ts index 5624058..36dce8c 100644 --- a/src/clients/conversation-messages-client.test.ts +++ b/src/clients/conversation-messages-client.test.ts @@ -2,11 +2,13 @@ import { http, HttpResponse } from 'msw' import { describe, expect, it } from 'vitest' import { CommsApi } from '../comms-api' import { server } from '../testUtils/msw-setup' -import { TEST_API_TOKEN, TEST_CONVERSATION_ID } from '../testUtils/test-defaults' +import { + TEST_API_BASE_URL as BASE, + TEST_API_TOKEN, + TEST_CONVERSATION_ID, +} from '../testUtils/test-defaults' import { generateId } from '../utils/uuidv7' -const BASE = 'https://comms.todoist.com/api/v1' - // The transport layer parses JSON, camelCases keys, and turns `*_ts` // epoch seconds into Date fields. Wire fixtures here use the raw // snake_case + `_ts` shape so the response goes through that pipeline diff --git a/src/testUtils/test-defaults.ts b/src/testUtils/test-defaults.ts index bc1b1fd..a0db4a3 100644 --- a/src/testUtils/test-defaults.ts +++ b/src/testUtils/test-defaults.ts @@ -10,6 +10,7 @@ import type { } from '../types/entities' export const TEST_API_TOKEN = 'test-api-token' +export const TEST_API_BASE_URL = 'https://comms.todoist.com/api/v1' // Canonical test IDs — shaped so they pass SDK-side validation. export const TEST_CHANNEL_ID = '7YpL3oZ4kZ9vP7Q1tR2sX3y' From 881f73c87e90fe04b8502cfbe8f589b0cd561504 Mon Sep 17 00:00:00 2001 From: Amir Date: Thu, 21 May 2026 13:40:14 +0200 Subject: [PATCH 4/5] fix: address Doistbot review on JSDoc/type mismatches and base URL helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sync client JSDoc with current request types (drop sendAsIntegration / recipients from create/update comment + thread docs, fix updateComment content required, fix UpdateUserArgs example name vs fullName, workspaceId required on getThreads and group add/remove ops, soften updatePassword.currentPassword wording). - Add `@param args` root descriptions to GroupsClient.getGroup / deleteGroup so JSDoc tooling parses the destructured args correctly. - Route BaseClient.getBaseUri through getCommsBaseUri so the default and custom-base paths share one URL builder; teach the helper to preserve the path on custom domains (e.g. `https://proxy/comms` → `.../comms/api/v1/`). - Derive TEST_API_BASE_URL from getCommsBaseUri() so the default host / version live in one place. - Add a request-level test that calls the SDK with a custom baseUrl and asserts the final request URL. --- src/clients/base-client.ts | 6 +----- src/clients/comments-client.ts | 4 +--- src/clients/groups-client.ts | 12 +++++++++++- src/clients/threads-client.ts | 14 ++++++++------ src/clients/users-client.ts | 7 ++++--- src/consts/endpoints.ts | 6 +++++- src/custom-fetch-simple.test.ts | 21 +++++++++++++++++++++ src/testUtils/test-defaults.ts | 3 ++- 8 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/clients/base-client.ts b/src/clients/base-client.ts index c071430..dbf19c8 100644 --- a/src/clients/base-client.ts +++ b/src/clients/base-client.ts @@ -36,10 +36,6 @@ export class BaseClient { * slash so relative paths resolve cleanly through `URL`. */ protected getBaseUri(): string { - if (this.baseUrl) { - const normalizedBaseUrl = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/` - return `${normalizedBaseUrl}api/${this.defaultVersion}/` - } - return getCommsBaseUri(this.defaultVersion) + return getCommsBaseUri(this.defaultVersion, this.baseUrl) } } diff --git a/src/clients/comments-client.ts b/src/clients/comments-client.ts index 40a92cf..21d929a 100644 --- a/src/clients/comments-client.ts +++ b/src/clients/comments-client.ts @@ -89,7 +89,6 @@ export class CommentsClient extends BaseClient { * `recipients` and `groups`. `'channel'` notifies everyone in the channel; * `'thread'` notifies everyone who has interacted with the thread. * @param args.attachments - Optional array of {@link Attachment} objects. - * @param args.sendAsIntegration - Optional flag to send as integration. * @returns The created comment object. * * @example @@ -115,8 +114,7 @@ export class CommentsClient extends BaseClient { * * @param args - The arguments for updating a comment. * @param args.id - The comment ID. - * @param args.content - Optional new comment content. - * @param args.recipients - Optional array of user IDs to notify. + * @param args.content - The new comment content. * @returns The updated comment object. */ updateComment(args: UpdateCommentArgs): Promise { diff --git a/src/clients/groups-client.ts b/src/clients/groups-client.ts index dbcf4ef..a057360 100644 --- a/src/clients/groups-client.ts +++ b/src/clients/groups-client.ts @@ -49,6 +49,7 @@ export class GroupsClient extends BaseClient { /** * Gets a single group object by id. Requires `workspaceId`. * + * @param args - The arguments for getting a group. * @param args.id - The group ID. * @param args.workspaceId - The workspace ID. * @returns The group object. @@ -109,6 +110,7 @@ export class GroupsClient extends BaseClient { /** * Permanently deletes a group. Requires `workspaceId`. * + * @param args - The arguments for deleting a group. * @param args.id - The group ID. * @param args.workspaceId - The workspace ID. */ @@ -121,6 +123,7 @@ export class GroupsClient extends BaseClient { * * @param args - The arguments for adding a user. * @param args.id - The group ID. + * @param args.workspaceId - The workspace ID. * @param args.userId - The user ID to add. */ addUser(args: AddGroupUserArgs): Promise { @@ -132,11 +135,16 @@ export class GroupsClient extends BaseClient { * * @param args - The arguments for adding users. * @param args.id - The group ID. + * @param args.workspaceId - The workspace ID. * @param args.userIds - Array of user IDs to add. * * @example * ```typescript - * await api.groups.addUsers({ id: '7YpL3oZ4kZ9vP7Q1tR2sX45', userIds: [101, 202, 303] }) + * await api.groups.addUsers({ + * id: '7YpL3oZ4kZ9vP7Q1tR2sX45', + * workspaceId: 123, + * userIds: [101, 202, 303], + * }) * ``` */ addUsers(args: AddGroupUsersArgs): Promise { @@ -148,6 +156,7 @@ export class GroupsClient extends BaseClient { * * @param args - The arguments for removing a user. * @param args.id - The group ID. + * @param args.workspaceId - The workspace ID. * @param args.userId - The user ID to remove. */ removeUser(args: RemoveGroupUserArgs): Promise { @@ -159,6 +168,7 @@ export class GroupsClient extends BaseClient { * * @param args - The arguments for removing users. * @param args.id - The group ID. + * @param args.workspaceId - The workspace ID. * @param args.userIds - Array of user IDs to remove. */ removeUsers(args: RemoveGroupUsersArgs): Promise { diff --git a/src/clients/threads-client.ts b/src/clients/threads-client.ts index 799750e..3144edd 100644 --- a/src/clients/threads-client.ts +++ b/src/clients/threads-client.ts @@ -46,8 +46,8 @@ export class ThreadsClient extends BaseClient { * `newer_than_ts` / `older_than_ts` epoch-second params on the wire. * * @param args - The arguments for getting threads. - * @param args.channelId - The channel ID. - * @param args.workspaceId - Optional workspace ID. + * @param args.workspaceId - The workspace ID. + * @param args.channelId - Optional channel ID to narrow to a single channel. * @param args.archived - Optional flag to include archived threads. * @param args.newerThan - Optional date to get threads newer than. * @param args.olderThan - Optional date to get threads older than. @@ -58,7 +58,10 @@ export class ThreadsClient extends BaseClient { * * @example * ```typescript - * const threads = await api.threads.getThreads({ channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44' }) + * const threads = await api.threads.getThreads({ + * workspaceId: 123, + * channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44', + * }) * threads.forEach(t => console.log(t.title)) * ``` */ @@ -97,10 +100,10 @@ export class ThreadsClient extends BaseClient { * * @param args - The arguments for creating a thread. * @param args.channelId - The channel ID. - * @param args.title - The thread title. + * @param args.title - Optional thread title. * @param args.content - The thread content. * @param args.recipients - Optional array of user IDs to notify. - * @param args.sendAsIntegration - Optional flag to send as integration. + * @param args.groups - Optional array of custom group IDs to notify. * @returns The created thread object. * * @example @@ -123,7 +126,6 @@ export class ThreadsClient extends BaseClient { * @param args.id - The thread ID. * @param args.title - Optional new thread title. * @param args.content - Optional new thread content. - * @param args.recipients - Optional array of user IDs to notify. * @returns The updated thread object. */ updateThread(args: UpdateThreadArgs): Promise { diff --git a/src/clients/users-client.ts b/src/clients/users-client.ts index 093d809..a866be0 100644 --- a/src/clients/users-client.ts +++ b/src/clients/users-client.ts @@ -198,7 +198,7 @@ export class UsersClient extends BaseClient { * @example * ```typescript * const user = await api.users.update({ - * fullName: 'John Doe', + * name: 'John Doe', * timezone: 'America/New_York', * }) * ``` @@ -208,11 +208,12 @@ export class UsersClient extends BaseClient { } /** - * Updates the user's password. Requires `currentPassword`. + * Updates the user's password. * * @param args - Password update arguments. * @param args.newPassword - The new password. - * @param args.currentPassword - The user's existing password (required to authenticate the change). + * @param args.currentPassword - The user's existing password. Optional — sent for + * re-authentication when the account has a password set. * @returns The updated user object. */ updatePassword(args: { newPassword: string; currentPassword?: string }): Promise { diff --git a/src/consts/endpoints.ts b/src/consts/endpoints.ts index d98463c..9d4ed3d 100644 --- a/src/consts/endpoints.ts +++ b/src/consts/endpoints.ts @@ -6,6 +6,9 @@ const BASE_URI = 'https://comms.todoist.com' /** * Gets the base URI for Comms API requests. * + * Preserves any path component on `domainBase` so callers can route through + * a proxy (e.g. `https://proxy.example.com/comms` → `.../comms/api/v1/`). + * * @param version - API version. Defaults to 'v1'. * @param domainBase - Custom domain base URL. Defaults to Comms' API domain. * @returns Complete base URI with trailing slash (e.g., 'https://comms.todoist.com/api/v1/') @@ -14,7 +17,8 @@ export function getCommsBaseUri( version: ApiVersion = DEFAULT_API_VERSION, domainBase: string = BASE_URI, ): string { - return new URL(`/api/${version}/`, domainBase).toString() + const base = domainBase.endsWith('/') ? domainBase : `${domainBase}/` + return new URL(`api/${version}/`, base).toString() } export const ENDPOINT_USERS = 'users' diff --git a/src/custom-fetch-simple.test.ts b/src/custom-fetch-simple.test.ts index 2f6d623..7f4acde 100644 --- a/src/custom-fetch-simple.test.ts +++ b/src/custom-fetch-simple.test.ts @@ -101,6 +101,27 @@ describe('Custom Fetch Core Functionality', () => { }), ) }) + + it('routes requests through a custom baseUrl with the configured version', async () => { + const mockCustomFetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + headers: { 'content-type': 'application/json' }, + text: () => Promise.resolve(JSON.stringify(mockUser)), + json: () => Promise.resolve(mockUser), + } as CustomFetchResponse) + + const api = new CommsApi(TEST_API_TOKEN, { + baseUrl: 'https://proxy.example.com/comms', + customFetch: mockCustomFetch, + }) + + await api.users.getSessionUser() + + const calledUrl = mockCustomFetch.mock.calls[0]?.[0] + expect(calledUrl).toBe('https://proxy.example.com/comms/api/v1/users/get_session_user') + }) }) describe('Authentication with Custom Fetch', () => { diff --git a/src/testUtils/test-defaults.ts b/src/testUtils/test-defaults.ts index a0db4a3..9a771a4 100644 --- a/src/testUtils/test-defaults.ts +++ b/src/testUtils/test-defaults.ts @@ -1,3 +1,4 @@ +import { getCommsBaseUri } from '../consts/endpoints' import type { Channel, Comment, @@ -10,7 +11,7 @@ import type { } from '../types/entities' export const TEST_API_TOKEN = 'test-api-token' -export const TEST_API_BASE_URL = 'https://comms.todoist.com/api/v1' +export const TEST_API_BASE_URL = getCommsBaseUri().replace(/\/$/, '') // Canonical test IDs — shaped so they pass SDK-side validation. export const TEST_CHANNEL_ID = '7YpL3oZ4kZ9vP7Q1tR2sX3y' From 089c03460cf0e58063922b94545e811119c62d8d Mon Sep 17 00:00:00 2001 From: Amir Date: Thu, 21 May 2026 13:41:05 +0200 Subject: [PATCH 5/5] test: route remaining test URLs through TEST_API_BASE_URL Per Scott's review on #4: the shared TEST_API_BASE_URL was only used in two test files. Replace the remaining hardcoded `apiUrl('api/v1/...')` calls in `add-comment-helper.test.ts` and `custom-fetch-simple.test.ts` so every API-side test goes through the same constant. Deprecated-param cleanup is intentionally deferred to a follow-up PR. --- src/clients/add-comment-helper.test.ts | 5 ++--- src/custom-fetch-simple.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/clients/add-comment-helper.test.ts b/src/clients/add-comment-helper.test.ts index f70f518..84cdc5c 100644 --- a/src/clients/add-comment-helper.test.ts +++ b/src/clients/add-comment-helper.test.ts @@ -1,14 +1,13 @@ import { http, HttpResponse } from 'msw' import { describe, expect, it } from 'vitest' import { getCommsBaseUri } from '../consts/endpoints' -import { apiUrl } from '../testUtils/msw-handlers' import { server } from '../testUtils/msw-setup' -import { TEST_API_TOKEN, TEST_THREAD_ID } from '../testUtils/test-defaults' +import { TEST_API_BASE_URL, TEST_API_TOKEN, TEST_THREAD_ID } from '../testUtils/test-defaults' import { EVERYONE, EVERYONE_IN_THREAD } from '../types/enums' import { addCommentRequest } from './add-comment-helper' const ctx = { baseUri: getCommsBaseUri(), apiToken: TEST_API_TOKEN } -const COMMENT_ADD = apiUrl('api/v1/comments/add') +const COMMENT_ADD = `${TEST_API_BASE_URL}/comments/add` const COMMENT_RESPONSE = { id: 'AAAAAAAAAAAAAAAAAAAAAA', diff --git a/src/custom-fetch-simple.test.ts b/src/custom-fetch-simple.test.ts index 7f4acde..410f504 100644 --- a/src/custom-fetch-simple.test.ts +++ b/src/custom-fetch-simple.test.ts @@ -1,9 +1,9 @@ import { http } from 'msw' import { beforeEach, describe, expect, it, vi } from 'vitest' import { CommsApi } from './comms-api' -import { apiUrl, createSuccessResponse } from './testUtils/msw-handlers' +import { createSuccessResponse } from './testUtils/msw-handlers' import { server } from './testUtils/msw-setup' -import { mockUser, TEST_API_TOKEN } from './testUtils/test-defaults' +import { mockUser, TEST_API_BASE_URL, TEST_API_TOKEN } from './testUtils/test-defaults' import type { CustomFetch, CustomFetchResponse } from './types/http' describe('Custom Fetch Core Functionality', () => { @@ -55,7 +55,7 @@ describe('Custom Fetch Core Functionality', () => { await api.users.getSessionUser() expect(mockCustomFetch).toHaveBeenCalledWith( - apiUrl('api/v1/users/get_session_user'), + `${TEST_API_BASE_URL}/users/get_session_user`, expect.objectContaining({ method: 'GET', headers: expect.objectContaining({ @@ -67,7 +67,7 @@ describe('Custom Fetch Core Functionality', () => { it('should use native fetch when no custom fetch provided', async () => { server.use( - http.get(apiUrl('api/v1/users/get_session_user'), () => { + http.get(`${TEST_API_BASE_URL}/users/get_session_user`, () => { return createSuccessResponse(mockUser) }), )