From dbb93bcb312047595eadea20c3744b519aead960 Mon Sep 17 00:00:00 2001 From: Scott Lovegrove Date: Thu, 9 Oct 2025 22:06:22 +0100 Subject: [PATCH 1/3] feat: Add initial batching support --- src/batch-builder.test.ts | 187 ++++++++++++++++++++++++++ src/batch-builder.ts | 174 ++++++++++++++++++++++++ src/clients/workspace-users-client.ts | 37 +++-- src/index.ts | 1 + src/twist-api.ts | 24 ++++ src/types/batch.ts | 31 +++++ src/types/index.ts | 1 + 7 files changed, 445 insertions(+), 10 deletions(-) create mode 100644 src/batch-builder.test.ts create mode 100644 src/batch-builder.ts create mode 100644 src/types/batch.ts diff --git a/src/batch-builder.test.ts b/src/batch-builder.test.ts new file mode 100644 index 0000000..f5db6b0 --- /dev/null +++ b/src/batch-builder.test.ts @@ -0,0 +1,187 @@ +import { HttpResponse, http } from 'msw' +import { describe, it, expect, beforeEach } from 'vitest' +import { server } from './testUtils/msw-setup' +import { TwistApi } from './twist-api' +import { TEST_API_TOKEN } from './testUtils/test-defaults' + +describe('BatchBuilder', () => { + let api: TwistApi + + beforeEach(() => { + api = new TwistApi(TEST_API_TOKEN) + }) + + describe('createBatch', () => { + it('should create a BatchBuilder instance', () => { + const batch = api.createBatch() + expect(batch).toBeDefined() + expect(typeof batch.add).toBe('function') + expect(typeof batch.execute).toBe('function') + }) + }) + + describe('add and execute', () => { + it('should batch multiple getUserById requests', async () => { + const mockUser1 = { + id: 456, + name: 'User One', + email: 'user1@example.com', + user_type: 'USER', + short_name: 'U1', + timezone: 'UTC', + removed: false, + bot: false, + version: 1, + } + + const mockUser2 = { + id: 789, + name: 'User Two', + email: 'user2@example.com', + user_type: 'USER', + short_name: 'U2', + timezone: 'UTC', + removed: false, + bot: false, + version: 1, + } + + server.use( + http.post('https://api.twist.com/api/v3/batch', async ({ request }) => { + const body = await request.text() + const params = new URLSearchParams(body) + const requestsStr = params.get('requests') + const parallel = params.get('parallel') + + expect(requestsStr).toBeDefined() + expect(parallel).toBe('true') // All GET requests + + const requests = JSON.parse(requestsStr!) + expect(requests).toHaveLength(2) + expect(requests[0].method).toBe('GET') + expect(requests[0].url).toContain('workspace_users/getone') + expect(requests[0].url).toContain('user_id=456') + expect(requests[1].url).toContain('user_id=789') + + return HttpResponse.json([ + { + code: 200, + headers: '', + body: JSON.stringify(mockUser1), + }, + { + code: 200, + headers: '', + body: JSON.stringify(mockUser2), + }, + ]) + }), + ) + + const batch = api.createBatch() + batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) + batch.add(() => api.workspaceUsers.getUserById(123, 789, { batch: true })) + + const results = await batch.execute() + + expect(results).toHaveLength(2) + expect(results[0].code).toBe(200) + expect(results[0].data.id).toBe(456) + expect(results[0].data.name).toBe('User One') + expect(results[1].code).toBe(200) + expect(results[1].data.id).toBe(789) + expect(results[1].data.name).toBe('User Two') + }) + + it('should handle empty batch', async () => { + const batch = api.createBatch() + const results = await batch.execute() + expect(results).toEqual([]) + }) + + it('should handle error responses in batch', async () => { + server.use( + http.post('https://api.twist.com/api/v3/batch', async () => { + return HttpResponse.json([ + { + code: 200, + headers: '', + body: JSON.stringify({ + id: 456, + name: 'User One', + email: 'user1@example.com', + user_type: 'USER', + short_name: 'U1', + timezone: 'UTC', + removed: false, + bot: false, + version: 1, + }), + }, + { + code: 404, + headers: '', + body: JSON.stringify({ error: 'User not found' }), + }, + ]) + }), + ) + + const batch = api.createBatch() + batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) + batch.add(() => api.workspaceUsers.getUserById(123, 999, { batch: true })) + + const results = await batch.execute() + + expect(results).toHaveLength(2) + expect(results[0].code).toBe(200) + expect(results[0].data.name).toBe('User One') + expect(results[1].code).toBe(404) + expect(results[1].data.error).toBe('User not found') + }) + + it('should support method chaining', () => { + const batch = api.createBatch() + const result = batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) + expect(result).toBe(batch) // Chaining returns the same instance + }) + }) + + describe('getUserById with batch option', () => { + it('should return descriptor when batch: true', () => { + const descriptor = api.workspaceUsers.getUserById(123, 456, { batch: true }) + + expect(descriptor).toEqual({ + method: 'GET', + url: 'workspace_users/getone', + params: { id: 123, user_id: 456 }, + schema: expect.any(Object), // WorkspaceUserSchema + }) + }) + + it('should return promise when batch is not specified', async () => { + server.use( + http.get('https://api.twist.com/api/v4/workspace_users/getone', async () => { + return HttpResponse.json({ + id: 456, + name: 'User One', + email: 'user1@example.com', + user_type: 'USER', + short_name: 'U1', + timezone: 'UTC', + removed: false, + bot: false, + version: 1, + }) + }), + ) + + const result = api.workspaceUsers.getUserById(123, 456) + expect(result).toBeInstanceOf(Promise) + + const user = await result + expect(user.id).toBe(456) + expect(user.name).toBe('User One') + }) + }) +}) diff --git a/src/batch-builder.ts b/src/batch-builder.ts new file mode 100644 index 0000000..9a921b7 --- /dev/null +++ b/src/batch-builder.ts @@ -0,0 +1,174 @@ +import { TwistRequestError } from './types/errors' +import { camelCaseKeys, snakeCaseKeys } from './utils/case-conversion' +import { transformTimestamps } from './utils/timestamp-conversion' +import type { BatchApiResponse, BatchRequestDescriptor, BatchResponse } from './types/batch' + +/** + * Builder for batching multiple API requests into a single HTTP call. + * + * @example + * ```typescript + * const batch = api.createBatch() + * batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) + * batch.add(() => api.workspaceUsers.getUserById(123, 789, { batch: true })) + * const results = await batch.execute() + * ``` + */ +export class BatchBuilder { + private requests: BatchRequestDescriptor[] = [] + + constructor( + private apiToken: string, + private baseUrl?: string, + ) {} + + private getBaseUri(): string { + return this.baseUrl ? `${this.baseUrl}/api/v3` : 'https://api.twist.com/api/v3/' + } + + /** + * Adds a request to the batch using a callback function. + * The callback receives the original API instance, but you must pass `{ batch: true }` as the last argument. + * + * @param fn - A function that receives the API client and returns a descriptor + * @returns This BatchBuilder instance for chaining + * + * @example + * ```typescript + * batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) + * ``` + */ + add(fn: () => BatchRequestDescriptor): this { + const descriptor = fn() + this.requests.push(descriptor) + return this + } + + /** + * Executes all batched requests in a single API call. + * + * @returns Array of BatchResponse objects with processed data + * @throws {TwistRequestError} If the batch request fails + */ + async execute(): Promise[]> { + if (this.requests.length === 0) { + return [] + } + + // Build the batch requests array + const batchRequests = this.requests.map((descriptor) => { + // Convert params to snake_case + const snakeCaseParams = descriptor.params + ? (snakeCaseKeys(descriptor.params) as Record) + : undefined + + // Build the full URL with query params for GET requests + let url = `${this.getBaseUri()}${descriptor.url}` + if (descriptor.method === 'GET' && snakeCaseParams) { + const searchParams = new URLSearchParams() + Object.entries(snakeCaseParams).forEach(([key, value]) => { + if (value != null) { + if (Array.isArray(value)) { + searchParams.append(key, value.join(',')) + } else { + searchParams.append(key, String(value)) + } + } + }) + const queryString = searchParams.toString() + if (queryString) { + url += `?${queryString}` + } + } + + return { + method: descriptor.method, + url, + } + }) + + // Check if all requests are GET (allows parallel execution) + const allGets = batchRequests.every((req) => req.method === 'GET') + + // Build form-urlencoded body + const formData = new URLSearchParams() + formData.append('requests', JSON.stringify(batchRequests)) + if (allGets) { + formData.append('parallel', 'true') + } + + // Execute the batch request + const response = await fetch(`${this.getBaseUri()}batch`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Bearer ${this.apiToken}`, + }, + body: formData.toString(), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new TwistRequestError( + `Batch request failed with status ${response.status}`, + response.status, + errorText, + ) + } + + const batchApiResponses: BatchApiResponse[] = await response.json() + + // Process each response + return batchApiResponses.map((apiResponse, index) => { + const descriptor = this.requests[index] + + // Parse the body JSON + let parsedBody: unknown + try { + parsedBody = apiResponse.body ? JSON.parse(apiResponse.body) : undefined + } catch (error) { + parsedBody = apiResponse.body + } + + // Apply transformations: camelCase -> timestamps + const camelCased = camelCaseKeys(parsedBody) + const transformed = transformTimestamps(camelCased) + + // Validate with schema if provided + let finalData = transformed + if (descriptor.schema && apiResponse.code >= 200 && apiResponse.code < 300) { + try { + finalData = descriptor.schema.parse(transformed) + } catch (error) { + // If validation fails, include the error in the response + console.error('Batch response validation failed:', error) + } + } + + // Parse headers string into object + const headers: Record = {} + if (apiResponse.headers && typeof apiResponse.headers === 'string') { + try { + const headerLines = apiResponse.headers.split('\n') + headerLines.forEach((line) => { + const separatorIndex = line.indexOf(':') + if (separatorIndex > 0) { + const key = line.substring(0, separatorIndex).trim() + const value = line.substring(separatorIndex + 1).trim() + headers[key] = value + } + }) + } catch (error) { + // If header parsing fails, just leave headers empty + console.error('Failed to parse batch response headers:', error) + } + } + + return { + code: apiResponse.code, + headers, + data: finalData, + } + }) + } +} diff --git a/src/clients/workspace-users-client.ts b/src/clients/workspace-users-client.ts index 85827af..78bfefd 100644 --- a/src/clients/workspace-users-client.ts +++ b/src/clients/workspace-users-client.ts @@ -1,4 +1,5 @@ import { request } from '../rest-client' +import { BatchRequestDescriptor } from '../types/batch' import { WorkspaceUser, WorkspaceUserSchema } from '../types/entities' import { UserType } from '../types/enums' @@ -63,24 +64,40 @@ export class WorkspaceUsersClient { * * @param workspaceId - The workspace ID. * @param userId - The user's ID. - * @returns The workspace user object. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. + * @returns The workspace user object or a batch request descriptor. * * @example * ```typescript + * // Normal usage * const user = await api.workspaceUsers.getUserById(123, 456) * console.log(user.name, user.email) + * + * // Batch usage + * const batch = api.createBatch() + * batch.add((api) => api.workspaceUsers.getUserById(123, 456)) + * const results = await batch.execute() * ``` */ - async getUserById(workspaceId: number, userId: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - 'workspace_users/getone', - this.apiToken, - { id: workspaceId, user_id: userId }, - ) + getUserById(workspaceId: number, userId: number, options: { batch: true }): BatchRequestDescriptor + getUserById(workspaceId: number, userId: number, options?: { batch?: false }): Promise + getUserById( + workspaceId: number, + userId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = 'workspace_users/getone' + const params = { id: workspaceId, user_id: userId } + const schema = WorkspaceUserSchema - return WorkspaceUserSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then((response) => + schema.parse(response.data), + ) } /** diff --git a/src/index.ts b/src/index.ts index e4a5c9f..4b46f23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './authentication' +export { BatchBuilder } from './batch-builder' export { ConversationMessagesClient } from './clients/conversation-messages-client' export { InboxClient } from './clients/inbox-client' export { ReactionsClient } from './clients/reactions-client' diff --git a/src/twist-api.ts b/src/twist-api.ts index 7d893d0..b75a505 100644 --- a/src/twist-api.ts +++ b/src/twist-api.ts @@ -1,3 +1,4 @@ +import { BatchBuilder } from './batch-builder' import { ChannelsClient } from './clients/channels-client' import { CommentsClient } from './clients/comments-client' import { ConversationMessagesClient } from './clients/conversation-messages-client' @@ -36,6 +37,9 @@ export class TwistApi { public reactions: ReactionsClient public search: SearchClient + private authToken: string + private baseUrl?: string + /** * Creates a new Twist API client. * @@ -43,6 +47,8 @@ export class TwistApi { * @param baseUrl - Optional custom API base URL. If not provided, defaults to Twist's standard API endpoint. */ constructor(authToken: string, baseUrl?: string) { + this.authToken = authToken + this.baseUrl = baseUrl this.users = new UsersClient(authToken, baseUrl) this.workspaces = new WorkspacesClient(authToken, baseUrl) this.workspaceUsers = new WorkspaceUsersClient(authToken, baseUrl) @@ -56,4 +62,22 @@ export class TwistApi { this.reactions = new ReactionsClient(authToken, baseUrl) this.search = new SearchClient(authToken, baseUrl) } + + /** + * Creates a batch builder for executing multiple API requests in a single HTTP call. + * + * @returns A BatchBuilder instance + * + * @example + * ```typescript + * const batch = api.createBatch() + * batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) + * batch.add(() => api.workspaceUsers.getUserById(123, 789, { batch: true })) + * const results = await batch.execute() + * console.log(results[0].data.name, results[1].data.name) + * ``` + */ + createBatch(): BatchBuilder { + return new BatchBuilder(this.authToken, this.baseUrl) + } } diff --git a/src/types/batch.ts b/src/types/batch.ts new file mode 100644 index 0000000..84642cf --- /dev/null +++ b/src/types/batch.ts @@ -0,0 +1,31 @@ +import { z } from 'zod' + +/** + * Descriptor for a batch request that captures the API call details + * without executing it. + */ +export type BatchRequestDescriptor = { + method: 'GET' | 'POST' + url: string + params?: Record + schema?: z.ZodSchema +} + +/** + * Response from a single request within a batch. + */ +export type BatchResponse = { + code: number + headers: Record + data: T +} + +/** + * Raw response format from the batch API endpoint. + * @internal + */ +export type BatchApiResponse = { + code: number + headers: string + body: string +} diff --git a/src/types/index.ts b/src/types/index.ts index 5d569ca..2539d36 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './batch' export * from './entities' export * from './enums' export * from './errors' From 63b43254c8a6d604f1dada860e84476dec9f010c Mon Sep 17 00:00:00 2001 From: Scott Lovegrove Date: Fri, 10 Oct 2025 08:22:35 +0100 Subject: [PATCH 2/3] feat: enhance API clients to support batch request descriptors --- src/batch-builder.test.ts | 49 +- src/batch-builder.ts | 51 +-- src/clients/channels-client.ts | 332 ++++++++++---- src/clients/comments-client.ts | 170 +++++-- src/clients/conversation-messages-client.ts | 149 ++++-- src/clients/conversations-client.ts | 436 +++++++++++++----- src/clients/groups-client.ts | 264 ++++++++--- src/clients/inbox-client.ts | 147 ++++-- src/clients/reactions-client.test.ts | 4 +- src/clients/reactions-client.ts | 76 ++- src/clients/search-client.ts | 89 +++- src/clients/threads-client.test.ts | 4 +- src/clients/threads-client.ts | 484 ++++++++++++++------ src/clients/users-client.ts | 346 +++++++++----- src/clients/workspace-users-client.ts | 374 +++++++++++---- src/clients/workspaces-client.ts | 192 +++++--- src/twist-api.ts | 21 +- src/types/batch.ts | 8 + tsconfig.json | 2 +- 19 files changed, 2299 insertions(+), 899 deletions(-) diff --git a/src/batch-builder.test.ts b/src/batch-builder.test.ts index f5db6b0..53ec56d 100644 --- a/src/batch-builder.test.ts +++ b/src/batch-builder.test.ts @@ -1,8 +1,8 @@ import { HttpResponse, http } from 'msw' -import { describe, it, expect, beforeEach } from 'vitest' +import { beforeEach, describe, expect, it } from 'vitest' import { server } from './testUtils/msw-setup' -import { TwistApi } from './twist-api' import { TEST_API_TOKEN } from './testUtils/test-defaults' +import { TwistApi } from './twist-api' describe('BatchBuilder', () => { let api: TwistApi @@ -11,12 +11,9 @@ describe('BatchBuilder', () => { api = new TwistApi(TEST_API_TOKEN) }) - describe('createBatch', () => { - it('should create a BatchBuilder instance', () => { - const batch = api.createBatch() - expect(batch).toBeDefined() - expect(typeof batch.add).toBe('function') - expect(typeof batch.execute).toBe('function') + describe('batch', () => { + it('should have a batch method', () => { + expect(typeof api.batch).toBe('function') }) }) @@ -78,11 +75,10 @@ describe('BatchBuilder', () => { }), ) - const batch = api.createBatch() - batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) - batch.add(() => api.workspaceUsers.getUserById(123, 789, { batch: true })) - - const results = await batch.execute() + const results = await api.batch( + api.workspaceUsers.getUserById(123, 456, { batch: true }), + api.workspaceUsers.getUserById(123, 789, { batch: true }), + ) expect(results).toHaveLength(2) expect(results[0].code).toBe(200) @@ -94,8 +90,7 @@ describe('BatchBuilder', () => { }) it('should handle empty batch', async () => { - const batch = api.createBatch() - const results = await batch.execute() + const results = await api.batch() expect(results).toEqual([]) }) @@ -127,23 +122,27 @@ describe('BatchBuilder', () => { }), ) - const batch = api.createBatch() - batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) - batch.add(() => api.workspaceUsers.getUserById(123, 999, { batch: true })) - - const results = await batch.execute() + const results = await api.batch( + api.workspaceUsers.getUserById(123, 456, { batch: true }), + api.workspaceUsers.getUserById(123, 999, { batch: true }), + ) expect(results).toHaveLength(2) expect(results[0].code).toBe(200) expect(results[0].data.name).toBe('User One') expect(results[1].code).toBe(404) - expect(results[1].data.error).toBe('User not found') + // Type assertion needed for error responses - error responses don't match the expected type + expect((results[1].data as unknown as { error: string }).error).toBe('User not found') }) - it('should support method chaining', () => { - const batch = api.createBatch() - const result = batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) - expect(result).toBe(batch) // Chaining returns the same instance + it('should accept array of descriptors', () => { + const descriptors = [ + api.workspaceUsers.getUserById(123, 456, { batch: true }), + api.workspaceUsers.getUserById(123, 789, { batch: true }), + ] + expect(descriptors).toHaveLength(2) + expect(descriptors[0].method).toBe('GET') + expect(descriptors[0].url).toBe('workspace_users/getone') }) }) diff --git a/src/batch-builder.ts b/src/batch-builder.ts index 9a921b7..37d5d41 100644 --- a/src/batch-builder.ts +++ b/src/batch-builder.ts @@ -1,22 +1,20 @@ +import type { BatchApiResponse, BatchRequestDescriptor, BatchResponseArray } from './types/batch' import { TwistRequestError } from './types/errors' import { camelCaseKeys, snakeCaseKeys } from './utils/case-conversion' import { transformTimestamps } from './utils/timestamp-conversion' -import type { BatchApiResponse, BatchRequestDescriptor, BatchResponse } from './types/batch' /** - * Builder for batching multiple API requests into a single HTTP call. + * Executes multiple API requests in a single HTTP call. * * @example * ```typescript - * const batch = api.createBatch() - * batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) - * batch.add(() => api.workspaceUsers.getUserById(123, 789, { batch: true })) - * const results = await batch.execute() + * const results = await api.batch( + * api.workspaceUsers.getUserById(123, 456, { batch: true }), + * api.workspaceUsers.getUserById(123, 789, { batch: true }) + * ) * ``` */ export class BatchBuilder { - private requests: BatchRequestDescriptor[] = [] - constructor( private apiToken: string, private baseUrl?: string, @@ -27,36 +25,21 @@ export class BatchBuilder { } /** - * Adds a request to the batch using a callback function. - * The callback receives the original API instance, but you must pass `{ batch: true }` as the last argument. - * - * @param fn - A function that receives the API client and returns a descriptor - * @returns This BatchBuilder instance for chaining - * - * @example - * ```typescript - * batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) - * ``` - */ - add(fn: () => BatchRequestDescriptor): this { - const descriptor = fn() - this.requests.push(descriptor) - return this - } - - /** - * Executes all batched requests in a single API call. + * Executes an array of batch request descriptors in a single API call. * + * @param requests - Array of batch request descriptors * @returns Array of BatchResponse objects with processed data * @throws {TwistRequestError} If the batch request fails */ - async execute(): Promise[]> { - if (this.requests.length === 0) { - return [] + async execute[]>( + requests: T, + ): Promise> { + if (requests.length === 0) { + return [] as BatchResponseArray } // Build the batch requests array - const batchRequests = this.requests.map((descriptor) => { + const batchRequests = requests.map((descriptor) => { // Convert params to snake_case const snakeCaseParams = descriptor.params ? (snakeCaseKeys(descriptor.params) as Record) @@ -120,13 +103,13 @@ export class BatchBuilder { // Process each response return batchApiResponses.map((apiResponse, index) => { - const descriptor = this.requests[index] + const descriptor = requests[index] // Parse the body JSON let parsedBody: unknown try { parsedBody = apiResponse.body ? JSON.parse(apiResponse.body) : undefined - } catch (error) { + } catch (_error) { parsedBody = apiResponse.body } @@ -169,6 +152,6 @@ export class BatchBuilder { headers, data: finalData, } - }) + }) as BatchResponseArray } } diff --git a/src/clients/channels-client.ts b/src/clients/channels-client.ts index 5c071e9..49a27a0 100644 --- a/src/clients/channels-client.ts +++ b/src/clients/channels-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_CHANNELS, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { Channel, ChannelSchema } from '../types/entities' import { CreateChannelArgs, GetChannelsArgs, UpdateChannelArgs } from '../types/requests' @@ -22,6 +23,7 @@ export class ChannelsClient { * @param args - The arguments for getting channels. * @param args.workspaceId - The workspace ID. * @param args.archived - Optional flag to include archived channels. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of channel objects. * * @example @@ -30,34 +32,50 @@ export class ChannelsClient { * channels.forEach(ch => console.log(ch.name)) * ``` */ - async getChannels(args: GetChannelsArgs): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_CHANNELS}/get`, - this.apiToken, - args, - ) + getChannels(args: GetChannelsArgs, options: { batch: true }): BatchRequestDescriptor + getChannels(args: GetChannelsArgs, options?: { batch?: false }): Promise + getChannels( + args: GetChannelsArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_CHANNELS}/get` + const params = args + + if (options?.batch) { + return { method, url, params } + } - return response.data.map((channel) => ChannelSchema.parse(channel)) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data.map((channel) => ChannelSchema.parse(channel)), + ) } /** * Gets a single channel object by id. * * @param id - The channel ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The channel object. */ - async getChannel(id: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_CHANNELS}/getone`, - this.apiToken, - { id }, - ) + getChannel(id: number, options: { batch: true }): BatchRequestDescriptor + getChannel(id: number, options?: { batch?: false }): Promise + getChannel( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_CHANNELS}/getone` + const params = { id } + const schema = ChannelSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return ChannelSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -70,6 +88,7 @@ export class ChannelsClient { * @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. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The created channel object. * * @example @@ -81,16 +100,27 @@ export class ChannelsClient { * }) * ``` */ - async createChannel(args: CreateChannelArgs): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CHANNELS}/add`, - this.apiToken, - args, - ) + createChannel( + args: CreateChannelArgs, + options: { batch: true }, + ): BatchRequestDescriptor + createChannel(args: CreateChannelArgs, options?: { batch?: false }): Promise + createChannel( + args: CreateChannelArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/add` + const params = args + const schema = ChannelSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return ChannelSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -102,73 +132,155 @@ export class ChannelsClient { * @param args.description - Optional new channel description. * @param args.color - Optional new channel color. * @param args.public - Optional flag to change channel visibility. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated channel object. */ - async updateChannel(args: UpdateChannelArgs): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CHANNELS}/update`, - this.apiToken, - args, - ) + updateChannel( + args: UpdateChannelArgs, + options: { batch: true }, + ): BatchRequestDescriptor + updateChannel(args: UpdateChannelArgs, options?: { batch?: false }): Promise + updateChannel( + args: UpdateChannelArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/update` + const params = args + const schema = ChannelSchema - return ChannelSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Permanently deletes a channel. * * @param id - The channel ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async deleteChannel(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_CHANNELS}/remove`, this.apiToken, { - id, - }) + deleteChannel(id: number, options: { batch: true }): BatchRequestDescriptor + deleteChannel(id: number, options?: { batch?: false }): Promise + deleteChannel( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/remove` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** * Archives a channel. * * @param id - The channel ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async archiveChannel(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_CHANNELS}/archive`, this.apiToken, { - id, - }) + archiveChannel(id: number, options: { batch: true }): BatchRequestDescriptor + archiveChannel(id: number, options?: { batch?: false }): Promise + archiveChannel( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/archive` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** * Unarchives a channel. * * @param id - The channel ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async unarchiveChannel(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_CHANNELS}/unarchive`, this.apiToken, { - id, - }) + unarchiveChannel(id: number, options: { batch: true }): BatchRequestDescriptor + unarchiveChannel(id: number, options?: { batch?: false }): Promise + unarchiveChannel( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/unarchive` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** * Favorites a channel. * * @param id - The channel ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async favoriteChannel(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_CHANNELS}/favorite`, this.apiToken, { - id, - }) + favoriteChannel(id: number, options: { batch: true }): BatchRequestDescriptor + favoriteChannel(id: number, options?: { batch?: false }): Promise + favoriteChannel( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/favorite` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** * Unfavorites a channel. * * @param id - The channel ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async unfavoriteChannel(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_CHANNELS}/unfavorite`, this.apiToken, { - id, - }) + unfavoriteChannel(id: number, options: { batch: true }): BatchRequestDescriptor + unfavoriteChannel(id: number, options?: { batch?: false }): Promise + unfavoriteChannel( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/unfavorite` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -176,17 +288,31 @@ export class ChannelsClient { * * @param id - The channel ID. * @param userId - The user ID to add. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.channels.addUser(456, 789) * ``` */ - async addUser(id: number, userId: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_CHANNELS}/add_user`, this.apiToken, { - id, - userId, - }) + addUser(id: number, userId: number, options: { batch: true }): BatchRequestDescriptor + addUser(id: number, userId: number, options?: { batch?: false }): Promise + addUser( + id: number, + userId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/add_user` + const params = { id, userId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -194,17 +320,31 @@ export class ChannelsClient { * * @param id - The channel ID. * @param userIds - Array of user IDs to add. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.channels.addUsers(456, [789, 790, 791]) * ``` */ - async addUsers(id: number, userIds: number[]): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_CHANNELS}/add_users`, this.apiToken, { - id, - userIds, - }) + addUsers(id: number, userIds: number[], options: { batch: true }): BatchRequestDescriptor + addUsers(id: number, userIds: number[], options?: { batch?: false }): Promise + addUsers( + id: number, + userIds: number[], + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/add_users` + const params = { id, userIds } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -212,17 +352,25 @@ export class ChannelsClient { * * @param id - The channel ID. * @param userId - The user ID to remove. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async removeUser(id: number, userId: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CHANNELS}/remove_user`, - this.apiToken, - { - id, - userId, - }, + removeUser(id: number, userId: number, options: { batch: true }): BatchRequestDescriptor + removeUser(id: number, userId: number, options?: { batch?: false }): Promise + removeUser( + id: number, + userId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/remove_user` + const params = { id, userId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } @@ -231,17 +379,29 @@ export class ChannelsClient { * * @param id - The channel ID. * @param userIds - Array of user IDs to remove. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async removeUsers(id: number, userIds: number[]): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CHANNELS}/remove_users`, - this.apiToken, - { - id, - userIds, - }, + removeUsers( + id: number, + userIds: number[], + options: { batch: true }, + ): BatchRequestDescriptor + removeUsers(id: number, userIds: number[], options?: { batch?: false }): Promise + removeUsers( + id: number, + userIds: number[], + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CHANNELS}/remove_users` + const params = { id, userIds } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } } diff --git a/src/clients/comments-client.ts b/src/clients/comments-client.ts index 483c69b..b06f1c2 100644 --- a/src/clients/comments-client.ts +++ b/src/clients/comments-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_COMMENTS, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { Comment, CommentSchema } from '../types/entities' import { CreateCommentArgs, GetCommentsArgs, UpdateCommentArgs } from '../types/requests' @@ -23,6 +24,7 @@ export class CommentsClient { * @param args.threadId - The thread ID. * @param args.from - Optional date to get comments from. * @param args.limit - Optional limit on number of comments returned. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of comment objects. * * @example @@ -34,7 +36,12 @@ export class CommentsClient { * comments.forEach(c => console.log(c.content)) * ``` */ - async getComments(args: GetCommentsArgs): Promise { + getComments(args: GetCommentsArgs, options: { batch: true }): BatchRequestDescriptor + getComments(args: GetCommentsArgs, options?: { batch?: false }): Promise + getComments( + args: GetCommentsArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { thread_id: args.threadId, } @@ -42,33 +49,43 @@ export class CommentsClient { if (args.from) params.from = Math.floor(args.from.getTime() / 1000) if (args.limit) params.limit = args.limit - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_COMMENTS}/get`, - this.apiToken, - params, - ) + const method = 'GET' + const url = `${ENDPOINT_COMMENTS}/get` + + if (options?.batch) { + return { method, url, params } + } - return response.data.map((comment) => CommentSchema.parse(comment)) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data.map((comment) => CommentSchema.parse(comment)), + ) } /** * Gets a single comment object by id. * * @param id - The comment ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The comment object. */ - async getComment(id: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_COMMENTS}/getone`, - this.apiToken, - { id }, - ) + getComment(id: number, options: { batch: true }): BatchRequestDescriptor + getComment(id: number, options?: { batch?: false }): Promise + getComment( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_COMMENTS}/getone` + const params = { id } + const schema = CommentSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return CommentSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -80,6 +97,7 @@ export class CommentsClient { * @param args.recipients - Optional array of user IDs to notify. * @param args.attachments - Optional array of attachment objects. * @param args.sendAsIntegration - Optional flag to send as integration. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The created comment object. * * @example @@ -90,16 +108,27 @@ export class CommentsClient { * }) * ``` */ - async createComment(args: CreateCommentArgs): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_COMMENTS}/add`, - this.apiToken, - args, - ) + createComment( + args: CreateCommentArgs, + options: { batch: true }, + ): BatchRequestDescriptor + createComment(args: CreateCommentArgs, options?: { batch?: false }): Promise + createComment( + args: CreateCommentArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_COMMENTS}/add` + const params = args + const schema = CommentSchema - return CommentSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -110,29 +139,55 @@ export class CommentsClient { * @param args.content - Optional new comment content. * @param args.recipients - Optional array of user IDs to notify. * @param args.attachments - Optional array of attachment objects. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated comment object. */ - async updateComment(args: UpdateCommentArgs): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_COMMENTS}/update`, - this.apiToken, - args, - ) + updateComment( + args: UpdateCommentArgs, + options: { batch: true }, + ): BatchRequestDescriptor + updateComment(args: UpdateCommentArgs, options?: { batch?: false }): Promise + updateComment( + args: UpdateCommentArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_COMMENTS}/update` + const params = args + const schema = CommentSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return CommentSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Permanently deletes a comment. * * @param id - The comment ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async deleteComment(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_COMMENTS}/remove`, this.apiToken, { - id, - }) + deleteComment(id: number, options: { batch: true }): BatchRequestDescriptor + deleteComment(id: number, options?: { batch?: false }): Promise + deleteComment( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_COMMENTS}/remove` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -141,22 +196,37 @@ export class CommentsClient { * * @param threadId - The thread ID. * @param commentId - The comment ID to mark as the last read position. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.comments.markPosition(789, 206113) * ``` */ - async markPosition(threadId: number, commentId: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_COMMENTS}/mark_position`, - this.apiToken, - { - thread_id: threadId, - comment_id: commentId, - }, + markPosition( + threadId: number, + commentId: number, + options: { batch: true }, + ): BatchRequestDescriptor + markPosition(threadId: number, commentId: number, options?: { batch?: false }): Promise + markPosition( + threadId: number, + commentId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_COMMENTS}/mark_position` + const params = { + thread_id: threadId, + comment_id: commentId, + } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } } diff --git a/src/clients/conversation-messages-client.ts b/src/clients/conversation-messages-client.ts index a60afae..613c748 100644 --- a/src/clients/conversation-messages-client.ts +++ b/src/clients/conversation-messages-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_CONVERSATION_MESSAGES, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { ConversationMessage, ConversationMessageSchema } from '../types/entities' type GetConversationMessagesArgs = { @@ -45,6 +46,7 @@ export class ConversationMessagesClient { * @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. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of message objects. * * @example @@ -55,7 +57,18 @@ export class ConversationMessagesClient { * }) * ``` */ - async getMessages(args: GetConversationMessagesArgs): Promise { + getMessages( + args: GetConversationMessagesArgs, + options: { batch: true }, + ): BatchRequestDescriptor + getMessages( + args: GetConversationMessagesArgs, + options?: { batch?: false }, + ): Promise + getMessages( + args: GetConversationMessagesArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { conversation_id: args.conversationId, } @@ -65,21 +78,29 @@ export class ConversationMessagesClient { if (args.limit) params.limit = args.limit if (args.cursor) params.cursor = args.cursor - const response = await request( - 'GET', + const method = 'GET' + const url = `${ENDPOINT_CONVERSATION_MESSAGES}/get` + + if (options?.batch) { + return { method, url, params } + } + + return request( + method, this.getBaseUri(), - `${ENDPOINT_CONVERSATION_MESSAGES}/get`, + url, this.apiToken, params, + ).then((response) => + response.data.map((message) => ConversationMessageSchema.parse(message)), ) - - return response.data.map((message) => ConversationMessageSchema.parse(message)) } /** * Gets a single conversation message by id. * * @param id - The message ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The conversation message object. * * @example @@ -87,16 +108,28 @@ export class ConversationMessagesClient { * const message = await api.conversationMessages.getMessage(514069) * ``` */ - async getMessage(id: number): Promise { - const response = await request( - 'GET', + getMessage(id: number, options: { batch: true }): BatchRequestDescriptor + getMessage(id: number, options?: { batch?: false }): Promise + getMessage( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_CONVERSATION_MESSAGES}/getone` + const params = { id } + const schema = ConversationMessageSchema + + if (options?.batch) { + return { method, url, params, schema } + } + + return request( + method, this.getBaseUri(), - `${ENDPOINT_CONVERSATION_MESSAGES}/getone`, + url, this.apiToken, - { id }, - ) - - return ConversationMessageSchema.parse(response.data) + params, + ).then((response) => schema.parse(response.data)) } /** @@ -107,6 +140,7 @@ export class ConversationMessagesClient { * @param args.content - The message content. * @param args.attachments - Optional array of attachment objects. * @param args.actions - Optional array of action objects. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The created message object. * * @example @@ -117,7 +151,18 @@ export class ConversationMessagesClient { * }) * ``` */ - async createMessage(args: CreateConversationMessageArgs): Promise { + createMessage( + args: CreateConversationMessageArgs, + options: { batch: true }, + ): BatchRequestDescriptor + createMessage( + args: CreateConversationMessageArgs, + options?: { batch?: false }, + ): Promise + createMessage( + args: CreateConversationMessageArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { conversation_id: args.conversationId, content: args.content, @@ -126,15 +171,21 @@ export class ConversationMessagesClient { if (args.attachments) params.attachments = args.attachments if (args.actions) params.actions = args.actions - const response = await request( - 'POST', + const method = 'POST' + const url = `${ENDPOINT_CONVERSATION_MESSAGES}/add` + const schema = ConversationMessageSchema + + if (options?.batch) { + return { method, url, params, schema } + } + + return request( + method, this.getBaseUri(), - `${ENDPOINT_CONVERSATION_MESSAGES}/add`, + url, this.apiToken, params, - ) - - return ConversationMessageSchema.parse(response.data) + ).then((response) => schema.parse(response.data)) } /** @@ -144,6 +195,7 @@ export class ConversationMessagesClient { * @param args.id - The message ID. * @param args.content - The new message content. * @param args.attachments - Optional array of attachment objects. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated message object. * * @example @@ -154,7 +206,18 @@ export class ConversationMessagesClient { * }) * ``` */ - async updateMessage(args: UpdateConversationMessageArgs): Promise { + updateMessage( + args: UpdateConversationMessageArgs, + options: { batch: true }, + ): BatchRequestDescriptor + updateMessage( + args: UpdateConversationMessageArgs, + options?: { batch?: false }, + ): Promise + updateMessage( + args: UpdateConversationMessageArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { id: args.id, content: args.content, @@ -162,34 +225,50 @@ export class ConversationMessagesClient { if (args.attachments) params.attachments = args.attachments - const response = await request( - 'POST', + const method = 'POST' + const url = `${ENDPOINT_CONVERSATION_MESSAGES}/update` + const schema = ConversationMessageSchema + + if (options?.batch) { + return { method, url, params, schema } + } + + return request( + method, this.getBaseUri(), - `${ENDPOINT_CONVERSATION_MESSAGES}/update`, + url, this.apiToken, params, - ) - - return ConversationMessageSchema.parse(response.data) + ).then((response) => schema.parse(response.data)) } /** * Permanently deletes a conversation message. * * @param id - The message ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.conversationMessages.deleteMessage(789) * ``` */ - async deleteMessage(id: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATION_MESSAGES}/remove`, - this.apiToken, - { id }, + deleteMessage(id: number, options: { batch: true }): BatchRequestDescriptor + deleteMessage(id: number, options?: { batch?: false }): Promise + deleteMessage( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATION_MESSAGES}/remove` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } } diff --git a/src/clients/conversations-client.ts b/src/clients/conversations-client.ts index 80574fd..3c34136 100644 --- a/src/clients/conversations-client.ts +++ b/src/clients/conversations-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_CONVERSATIONS, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { Conversation, ConversationSchema, @@ -27,6 +28,7 @@ export class ConversationsClient { * @param args - The arguments for getting conversations. * @param args.workspaceId - The workspace ID. * @param args.archived - Optional flag to include archived conversations. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of conversation objects. * * @example @@ -35,34 +37,57 @@ export class ConversationsClient { * conversations.forEach(c => console.log(c.title)) * ``` */ - async getConversations(args: GetConversationsArgs): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/get`, - this.apiToken, - args, - ) + getConversations( + args: GetConversationsArgs, + options: { batch: true }, + ): BatchRequestDescriptor + getConversations( + args: GetConversationsArgs, + options?: { batch?: false }, + ): Promise + getConversations( + args: GetConversationsArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_CONVERSATIONS}/get` + const params = args - return response.data.map((conversation) => ConversationSchema.parse(conversation)) + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => + response.data.map((conversation) => ConversationSchema.parse(conversation)), + ) } /** * Gets a single conversation object by id. * * @param id - The conversation ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The conversation object. */ - async getConversation(id: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/getone`, - this.apiToken, - { id }, - ) + getConversation(id: number, options: { batch: true }): BatchRequestDescriptor + getConversation(id: number, options?: { batch?: false }): Promise + getConversation( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_CONVERSATIONS}/getone` + const params = { id } + const schema = ConversationSchema - return ConversationSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -71,6 +96,7 @@ export class ConversationsClient { * @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. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The conversation object (existing or newly created). * * @example @@ -81,16 +107,30 @@ export class ConversationsClient { * }) * ``` */ - async getOrCreateConversation(args: GetOrCreateConversationArgs): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/get_or_create`, - this.apiToken, - args, - ) + getOrCreateConversation( + args: GetOrCreateConversationArgs, + options: { batch: true }, + ): BatchRequestDescriptor + getOrCreateConversation( + args: GetOrCreateConversationArgs, + options?: { batch?: false }, + ): Promise + getOrCreateConversation( + args: GetOrCreateConversationArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/get_or_create` + const params = args + const schema = ConversationSchema - return ConversationSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -99,6 +139,7 @@ export class ConversationsClient { * @param id - The conversation ID. * @param title - The new title for the conversation. * @param archived - Optional flag to archive/unarchive the conversation. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated conversation object. * * @example @@ -106,49 +147,84 @@ export class ConversationsClient { * const conversation = await api.conversations.updateConversation(123, 'New Title') * ``` */ - async updateConversation(id: number, title: string, archived?: boolean): Promise { + updateConversation( + id: number, + title: string, + archived: boolean | undefined, + options: { batch: true }, + ): BatchRequestDescriptor + updateConversation( + id: number, + title: string, + archived?: boolean, + options?: { batch?: false }, + ): Promise + updateConversation( + id: number, + title: string, + archived?: boolean, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { id, title } if (archived !== undefined) params.archived = archived - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/update`, - this.apiToken, - params, - ) + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/update` + const schema = ConversationSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return ConversationSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Archives a conversation. * * @param id - The conversation ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async archiveConversation(id: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/archive`, - this.apiToken, - { id }, - ) + archiveConversation(id: number, options: { batch: true }): BatchRequestDescriptor + archiveConversation(id: number, options?: { batch?: false }): Promise + archiveConversation( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/archive` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Unarchives a conversation. * * @param id - The conversation ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async unarchiveConversation(id: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/unarchive`, - this.apiToken, - { id }, - ) + unarchiveConversation(id: number, options: { batch: true }): BatchRequestDescriptor + unarchiveConversation(id: number, options?: { batch?: false }): Promise + unarchiveConversation( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/unarchive` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -156,15 +232,24 @@ export class ConversationsClient { * * @param id - The conversation ID. * @param userId - The user ID to add. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async addUser(id: number, userId: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/add_user`, - this.apiToken, - { id, userId }, - ) + addUser(id: number, userId: number, options: { batch: true }): BatchRequestDescriptor + addUser(id: number, userId: number, options?: { batch?: false }): Promise + addUser( + id: number, + userId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/add_user` + const params = { id, userId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -172,20 +257,29 @@ export class ConversationsClient { * * @param id - The conversation ID. * @param userIds - Array of user IDs to add. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.conversations.addUsers(123, [456, 789, 101]) * ``` */ - async addUsers(id: number, userIds: number[]): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/add_users`, - this.apiToken, - { id, userIds }, - ) + addUsers(id: number, userIds: number[], options: { batch: true }): BatchRequestDescriptor + addUsers(id: number, userIds: number[], options?: { batch?: false }): Promise + addUsers( + id: number, + userIds: number[], + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/add_users` + const params = { id, userIds } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -193,15 +287,24 @@ export class ConversationsClient { * * @param id - The conversation ID. * @param userId - The user ID to remove. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async removeUser(id: number, userId: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/remove_user`, - this.apiToken, - { id, userId }, - ) + removeUser(id: number, userId: number, options: { batch: true }): BatchRequestDescriptor + removeUser(id: number, userId: number, options?: { batch?: false }): Promise + removeUser( + id: number, + userId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/remove_user` + const params = { id, userId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -209,15 +312,28 @@ export class ConversationsClient { * * @param id - The conversation ID. * @param userIds - Array of user IDs to remove. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async removeUsers(id: number, userIds: number[]): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/remove_users`, - this.apiToken, - { id, userIds }, - ) + removeUsers( + id: number, + userIds: number[], + options: { batch: true }, + ): BatchRequestDescriptor + removeUsers(id: number, userIds: number[], options?: { batch?: false }): Promise + removeUsers( + id: number, + userIds: number[], + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/remove_users` + const params = { id, userIds } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -227,19 +343,32 @@ export class ConversationsClient { * @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. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async markRead(args: { id: number; objIndex?: number; messageId?: number }): Promise { + markRead( + args: { id: number; objIndex?: number; messageId?: number }, + options: { batch: true }, + ): BatchRequestDescriptor + markRead( + args: { id: number; objIndex?: number; messageId?: number }, + options?: { batch?: false }, + ): Promise + markRead( + args: { id: number; objIndex?: number; messageId?: number }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { id: args.id } if (args.objIndex !== undefined) params.obj_index = args.objIndex if (args.messageId !== undefined) params.message_id = args.messageId - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/mark_read`, - this.apiToken, - params, - ) + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/mark_read` + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -249,37 +378,67 @@ export class ConversationsClient { * @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. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async markUnread(args: { id: number; objIndex?: number; messageId?: number }): Promise { + markUnread( + args: { id: number; objIndex?: number; messageId?: number }, + options: { batch: true }, + ): BatchRequestDescriptor + markUnread( + args: { id: number; objIndex?: number; messageId?: number }, + options?: { batch?: false }, + ): Promise + markUnread( + args: { id: number; objIndex?: number; messageId?: number }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { id: args.id } if (args.objIndex !== undefined) params.obj_index = args.objIndex if (args.messageId !== undefined) params.message_id = args.messageId - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/mark_unread`, - this.apiToken, - params, - ) + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/mark_unread` + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Gets unread conversations for a workspace. * * @param workspaceId - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Array of unread conversation references. */ - async getUnread(workspaceId: number): Promise { - const response = await request( - 'GET', + getUnread( + workspaceId: number, + options: { batch: true }, + ): BatchRequestDescriptor + getUnread(workspaceId: number, options?: { batch?: false }): Promise + getUnread( + workspaceId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_CONVERSATIONS}/get_unread` + const params = { workspace_id: workspaceId } + + if (options?.batch) { + return { method, url, params } + } + + return request( + method, this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/get_unread`, + url, this.apiToken, - { workspace_id: workspaceId }, + params, + ).then((response) => + response.data.map((conversation) => UnreadConversationSchema.parse(conversation)), ) - - return response.data.map((conversation) => UnreadConversationSchema.parse(conversation)) } /** @@ -288,6 +447,7 @@ export class ConversationsClient { * * @param id - The conversation ID. * @param minutes - Number of minutes to mute the conversation. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated conversation object. * * @example @@ -295,33 +455,59 @@ export class ConversationsClient { * const conversation = await api.conversations.muteConversation(123, 30) * ``` */ - async muteConversation(id: number, minutes: number): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/mute`, - this.apiToken, - { id, minutes }, - ) + muteConversation( + id: number, + minutes: number, + options: { batch: true }, + ): BatchRequestDescriptor + muteConversation( + id: number, + minutes: number, + options?: { batch?: false }, + ): Promise + muteConversation( + id: number, + minutes: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/mute` + const params = { id, minutes } + const schema = ConversationSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return ConversationSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Unmutes a conversation. * * @param id - The conversation ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated conversation object. */ - async unmuteConversation(id: number): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_CONVERSATIONS}/unmute`, - this.apiToken, - { id }, - ) + unmuteConversation(id: number, options: { batch: true }): BatchRequestDescriptor + unmuteConversation(id: number, options?: { batch?: false }): Promise + unmuteConversation( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_CONVERSATIONS}/unmute` + const params = { id } + const schema = ConversationSchema - return ConversationSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } } diff --git a/src/clients/groups-client.ts b/src/clients/groups-client.ts index a196551..b6bf4d6 100644 --- a/src/clients/groups-client.ts +++ b/src/clients/groups-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_GROUPS, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { Group, GroupSchema } from '../types/entities' /** @@ -19,6 +20,7 @@ export class GroupsClient { * Gets all groups for a given workspace. * * @param workspaceId - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of group objects. * * @example @@ -27,34 +29,50 @@ export class GroupsClient { * groups.forEach(g => console.log(g.name)) * ``` */ - async getGroups(workspaceId: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_GROUPS}/get`, - this.apiToken, - { workspaceId }, - ) + getGroups(workspaceId: number, options: { batch: true }): BatchRequestDescriptor + getGroups(workspaceId: number, options?: { batch?: false }): Promise + getGroups( + workspaceId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_GROUPS}/get` + const params = { workspaceId } + + if (options?.batch) { + return { method, url, params } + } - return response.data.map((group) => GroupSchema.parse(group)) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data.map((group) => GroupSchema.parse(group)), + ) } /** * Gets a single group object by id. * * @param id - The group ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The group object. */ - async getGroup(id: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_GROUPS}/getone`, - this.apiToken, - { id }, - ) + getGroup(id: number, options: { batch: true }): BatchRequestDescriptor + getGroup(id: number, options?: { batch?: false }): Promise + getGroup( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_GROUPS}/getone` + const params = { id } + const schema = GroupSchema - return GroupSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -66,6 +84,7 @@ export class GroupsClient { * @param args.description - Optional group description. * @param args.color - Optional group color. * @param args.userIds - Optional array of user IDs to add to the group. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The created group object. * * @example @@ -77,22 +96,45 @@ export class GroupsClient { * }) * ``` */ - async createGroup(args: { + createGroup( + args: { + workspaceId: number + name: string + description?: string + color?: string + userIds?: number[] + }, + options: { batch: true }, + ): BatchRequestDescriptor + createGroup(args: { workspaceId: number name: string description?: string color?: string userIds?: number[] - }): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_GROUPS}/add`, - this.apiToken, - args, - ) + }): Promise + createGroup( + args: { + workspaceId: number + name: string + description?: string + color?: string + userIds?: number[] + }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_GROUPS}/add` + const params = args + const schema = GroupSchema - return GroupSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -103,32 +145,70 @@ export class GroupsClient { * @param args.name - Optional new group name. * @param args.description - Optional new group description. * @param args.color - Optional new group color. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated group object. */ - async updateGroup(args: { + updateGroup( + args: { + id: number + name?: string + description?: string + color?: string + }, + options: { batch: true }, + ): BatchRequestDescriptor + updateGroup(args: { id: number name?: string description?: string color?: string - }): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_GROUPS}/update`, - this.apiToken, - args, - ) + }): Promise + updateGroup( + args: { + id: number + name?: string + description?: string + color?: string + }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_GROUPS}/update` + const params = args + const schema = GroupSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return GroupSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Permanently deletes a group. * * @param id - The group ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async deleteGroup(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_GROUPS}/remove`, this.apiToken, { id }) + deleteGroup(id: number, options: { batch: true }): BatchRequestDescriptor + deleteGroup(id: number, options?: { batch?: false }): Promise + deleteGroup( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_GROUPS}/remove` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -136,12 +216,30 @@ export class GroupsClient { * * @param id - The group ID. * @param userId - The user ID to add. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async addUserToGroup(id: number, userId: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_GROUPS}/add_user`, this.apiToken, { - id, - userId, - }) + addUserToGroup( + id: number, + userId: number, + options: { batch: true }, + ): BatchRequestDescriptor + addUserToGroup(id: number, userId: number, options?: { batch?: false }): Promise + addUserToGroup( + id: number, + userId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_GROUPS}/add_user` + const params = { id, userId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -149,17 +247,31 @@ export class GroupsClient { * * @param id - The group ID. * @param userIds - Array of user IDs to add. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.groups.addUsers(123, [456, 789, 101]) * ``` */ - async addUsers(id: number, userIds: number[]): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_GROUPS}/add_users`, this.apiToken, { - id, - userIds, - }) + addUsers(id: number, userIds: number[], options: { batch: true }): BatchRequestDescriptor + addUsers(id: number, userIds: number[], options?: { batch?: false }): Promise + addUsers( + id: number, + userIds: number[], + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_GROUPS}/add_users` + const params = { id, userIds } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -167,12 +279,26 @@ export class GroupsClient { * * @param id - The group ID. * @param userId - The user ID to remove. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async removeUser(id: number, userId: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_GROUPS}/remove_user`, this.apiToken, { - id, - userId, - }) + removeUser(id: number, userId: number, options: { batch: true }): BatchRequestDescriptor + removeUser(id: number, userId: number, options?: { batch?: false }): Promise + removeUser( + id: number, + userId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_GROUPS}/remove_user` + const params = { id, userId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -180,11 +306,29 @@ export class GroupsClient { * * @param id - The group ID. * @param userIds - Array of user IDs to remove. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async removeUsers(id: number, userIds: number[]): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_GROUPS}/remove_users`, this.apiToken, { - id, - userIds, - }) + removeUsers( + id: number, + userIds: number[], + options: { batch: true }, + ): BatchRequestDescriptor + removeUsers(id: number, userIds: number[], options?: { batch?: false }): Promise + removeUsers( + id: number, + userIds: number[], + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_GROUPS}/remove_users` + const params = { id, userIds } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } } diff --git a/src/clients/inbox-client.ts b/src/clients/inbox-client.ts index 2f2566d..c20f396 100644 --- a/src/clients/inbox-client.ts +++ b/src/clients/inbox-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_INBOX, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { InboxThread, InboxThreadSchema } from '../types/entities' type GetInboxArgs = { @@ -44,6 +45,7 @@ export class InboxClient { * @param args.until - Optional date to get items until. * @param args.limit - Optional limit on number of items returned. * @param args.cursor - Optional cursor for pagination. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Inbox threads. * * @example @@ -54,7 +56,12 @@ export class InboxClient { * }) * ``` */ - async getInbox(args: GetInboxArgs): Promise { + getInbox(args: GetInboxArgs, options: { batch: true }): BatchRequestDescriptor + getInbox(args: GetInboxArgs, options?: { batch?: false }): Promise + getInbox( + args: GetInboxArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { workspace_id: args.workspaceId, } @@ -64,21 +71,23 @@ export class InboxClient { if (args.limit) params.limit = args.limit if (args.cursor) params.cursor = args.cursor - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_INBOX}/get`, - this.apiToken, - params, - ) + const method = 'GET' + const url = `${ENDPOINT_INBOX}/get` - return response.data.map((thread) => InboxThreadSchema.parse(thread)) + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data.map((thread) => InboxThreadSchema.parse(thread)), + ) } /** * Gets unread count for inbox. * * @param workspaceId - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The unread count. * * @example @@ -87,51 +96,86 @@ export class InboxClient { * console.log(`Unread items: ${count}`) * ``` */ - async getCount(workspaceId: number): Promise { - const response = await request( - 'GET', + getCount(workspaceId: number, options: { batch: true }): BatchRequestDescriptor + getCount(workspaceId: number, options?: { batch?: false }): Promise + getCount( + workspaceId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_INBOX}/get_count` + const params = { workspace_id: workspaceId } + + if (options?.batch) { + return { method, url, params } + } + + return request( + method, this.getBaseUri(), - `${ENDPOINT_INBOX}/get_count`, + url, this.apiToken, - { workspace_id: workspaceId }, - ) - - return response.data.data + params, + ).then((response) => response.data.data) } /** * Archives a thread in the inbox. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.inbox.archiveThread(456) * ``` */ - async archiveThread(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_INBOX}/archive`, this.apiToken, { - id, - }) + archiveThread(id: number, options: { batch: true }): BatchRequestDescriptor + archiveThread(id: number, options?: { batch?: false }): Promise + archiveThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_INBOX}/archive` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** * Unarchives a thread in the inbox. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.inbox.unarchiveThread(456) * ``` */ - async unarchiveThread(id: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_INBOX}/unarchive`, - this.apiToken, - { id }, + unarchiveThread(id: number, options: { batch: true }): BatchRequestDescriptor + unarchiveThread(id: number, options?: { batch?: false }): Promise + unarchiveThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_INBOX}/unarchive` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } @@ -139,19 +183,29 @@ export class InboxClient { * Marks all inbox items as read in a workspace. * * @param workspaceId - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.inbox.markAllRead(123) * ``` */ - async markAllRead(workspaceId: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_INBOX}/mark_all_read`, - this.apiToken, - { workspace_id: workspaceId }, + markAllRead(workspaceId: number, options: { batch: true }): BatchRequestDescriptor + markAllRead(workspaceId: number, options?: { batch?: false }): Promise + markAllRead( + workspaceId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_INBOX}/mark_all_read` + const params = { workspace_id: workspaceId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } @@ -163,6 +217,7 @@ export class InboxClient { * @param args.channelIds - Optional array of channel IDs to filter by. * @param args.since - Optional date to filter items since. * @param args.until - Optional date to filter items until. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript @@ -172,7 +227,12 @@ export class InboxClient { * }) * ``` */ - async archiveAll(args: ArchiveAllArgs): Promise { + archiveAll(args: ArchiveAllArgs, options: { batch: true }): BatchRequestDescriptor + archiveAll(args: ArchiveAllArgs, options?: { batch?: false }): Promise + archiveAll( + args: ArchiveAllArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { workspace_id: args.workspaceId, } @@ -181,12 +241,15 @@ export class InboxClient { if (args.since) params.since_ts_or_obj_idx = Math.floor(args.since.getTime() / 1000) if (args.until) params.until_ts_or_obj_idx = Math.floor(args.until.getTime() / 1000) - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_INBOX}/archive_all`, - this.apiToken, - params, + const method = 'POST' + const url = `${ENDPOINT_INBOX}/archive_all` + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } } diff --git a/src/clients/reactions-client.test.ts b/src/clients/reactions-client.test.ts index 139eed2..657963d 100644 --- a/src/clients/reactions-client.test.ts +++ b/src/clients/reactions-client.test.ts @@ -58,8 +58,8 @@ describe('ReactionsClient', () => { expect(result).toBeNull() }) - it('should throw error when no id provided', async () => { - await expect(client.get({})).rejects.toThrow( + it('should throw error when no id provided', () => { + expect(() => client.get({})).toThrow( 'Must provide one of: threadId, commentId, or messageId', ) }) diff --git a/src/clients/reactions-client.ts b/src/clients/reactions-client.ts index b6b3bb4..c848cf1 100644 --- a/src/clients/reactions-client.ts +++ b/src/clients/reactions-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_REACTIONS, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' type AddReactionArgs = { threadId?: number @@ -44,13 +45,23 @@ export class ReactionsClient { * @param args.commentId - Optional comment ID. * @param args.messageId - Optional message ID (for conversation messages). * @param args.reaction - The reaction emoji to add. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.reactions.add({ threadId: 789, reaction: '👍' }) + * + * // Batch usage + * const batch = api.createBatch() + * batch.add(() => api.reactions.add({ threadId: 789, reaction: '👍' }, { batch: true })) * ``` */ - async add(args: AddReactionArgs): Promise { + add(args: AddReactionArgs, options: { batch: true }): BatchRequestDescriptor + add(args: AddReactionArgs, options?: { batch?: false }): Promise + add( + args: AddReactionArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { reaction: args.reaction, } @@ -65,12 +76,15 @@ export class ReactionsClient { throw new Error('Must provide one of: threadId, commentId, or messageId') } - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_REACTIONS}/add`, - this.apiToken, - params, + const method = 'POST' + const url = `${ENDPOINT_REACTIONS}/add` + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } @@ -81,6 +95,7 @@ export class ReactionsClient { * @param args.threadId - Optional thread ID. * @param args.commentId - Optional comment ID. * @param args.messageId - Optional message ID (for conversation messages). + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns A reaction object with emoji reactions as keys and arrays of user IDs as values, or null if no reactions. * * @example @@ -89,7 +104,12 @@ export class ReactionsClient { * // Returns: { "👍": [1, 2, 3], "❤️": [4, 5] } * ``` */ - async get(args: GetReactionsArgs): Promise { + get(args: GetReactionsArgs, options: { batch: true }): BatchRequestDescriptor + get(args: GetReactionsArgs, options?: { batch?: false }): Promise + get( + args: GetReactionsArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = {} if (args.threadId) { @@ -102,15 +122,16 @@ export class ReactionsClient { throw new Error('Must provide one of: threadId, commentId, or messageId') } - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_REACTIONS}/get`, - this.apiToken, - params, - ) + const method = 'POST' + const url = `${ENDPOINT_REACTIONS}/get` - return response.data + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data, + ) } /** @@ -121,13 +142,19 @@ export class ReactionsClient { * @param args.commentId - Optional comment ID. * @param args.messageId - Optional message ID (for conversation messages). * @param args.reaction - The reaction emoji to remove. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.reactions.remove({ threadId: 789, reaction: '👍' }) * ``` */ - async remove(args: RemoveReactionArgs): Promise { + remove(args: RemoveReactionArgs, options: { batch: true }): BatchRequestDescriptor + remove(args: RemoveReactionArgs, options?: { batch?: false }): Promise + remove( + args: RemoveReactionArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { reaction: args.reaction, } @@ -142,12 +169,15 @@ export class ReactionsClient { throw new Error('Must provide one of: threadId, commentId, or messageId') } - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_REACTIONS}/remove`, - this.apiToken, - params, + const method = 'POST' + const url = `${ENDPOINT_REACTIONS}/remove` + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, ) } } diff --git a/src/clients/search-client.ts b/src/clients/search-client.ts index 56a7a01..74160ee 100644 --- a/src/clients/search-client.ts +++ b/src/clients/search-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_SEARCH, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { SearchResult, SearchResultSchema } from '../types/entities' type SearchArgs = { @@ -69,6 +70,7 @@ export class SearchClient { * @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. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Search results with pagination. * * @example @@ -79,7 +81,12 @@ export class SearchClient { * }) * ``` */ - async search(args: SearchArgs): Promise { + search(args: SearchArgs, options: { batch: true }): BatchRequestDescriptor + search(args: SearchArgs, options?: { batch?: false }): Promise + search( + args: SearchArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { query: args.query, workspace_id: args.workspaceId, @@ -93,18 +100,19 @@ export class SearchClient { if (args.limit) params.limit = args.limit if (args.cursor) params.cursor = args.cursor - const response = await request( - 'GET', - this.getBaseUri(), - ENDPOINT_SEARCH, - this.apiToken, - params, - ) + const method = 'GET' + const url = ENDPOINT_SEARCH - return { - ...response.data, - items: response.data.items.map((result) => SearchResultSchema.parse(result)), + if (options?.batch) { + return { method, url, params } } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => ({ + ...response.data, + items: response.data.items.map((result) => SearchResultSchema.parse(result)), + }), + ) } /** @@ -115,6 +123,7 @@ export class SearchClient { * @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. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Comment IDs that match the search query. * * @example @@ -125,7 +134,15 @@ export class SearchClient { * }) * ``` */ - async searchThread(args: SearchThreadArgs): Promise { + searchThread( + args: SearchThreadArgs, + options: { batch: true }, + ): BatchRequestDescriptor + searchThread(args: SearchThreadArgs, options?: { batch?: false }): Promise + searchThread( + args: SearchThreadArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { query: args.query, thread_id: args.threadId, @@ -134,15 +151,20 @@ export class SearchClient { if (args.limit) params.limit = args.limit if (args.cursor) params.cursor = args.cursor - const response = await request( - 'GET', + const method = 'GET' + const url = `${ENDPOINT_SEARCH}/thread` + + if (options?.batch) { + return { method, url, params } + } + + return request( + method, this.getBaseUri(), - `${ENDPOINT_SEARCH}/thread`, + url, this.apiToken, params, - ) - - return response.data + ).then((response) => response.data) } /** @@ -153,6 +175,7 @@ export class SearchClient { * @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. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Message IDs that match the search query. * * @example @@ -163,7 +186,18 @@ export class SearchClient { * }) * ``` */ - async searchConversation(args: SearchConversationArgs): Promise { + searchConversation( + args: SearchConversationArgs, + options: { batch: true }, + ): BatchRequestDescriptor + searchConversation( + args: SearchConversationArgs, + options?: { batch?: false }, + ): Promise + searchConversation( + args: SearchConversationArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { query: args.query, conversation_id: args.conversationId, @@ -172,14 +206,19 @@ export class SearchClient { if (args.limit) params.limit = args.limit if (args.cursor) params.cursor = args.cursor - const response = await request( - 'GET', + const method = 'GET' + const url = `${ENDPOINT_SEARCH}/conversation` + + if (options?.batch) { + return { method, url, params } + } + + return request( + method, this.getBaseUri(), - `${ENDPOINT_SEARCH}/conversation`, + url, this.apiToken, params, - ) - - return response.data + ).then((response) => response.data) } } diff --git a/src/clients/threads-client.test.ts b/src/clients/threads-client.test.ts index 8a3e042..901d7bd 100644 --- a/src/clients/threads-client.test.ts +++ b/src/clients/threads-client.test.ts @@ -157,8 +157,8 @@ describe('ThreadsClient', () => { await client.markAllRead({ channelId: 456 }) }) - it('should throw error if neither workspaceId nor channelId provided', async () => { - await expect(client.markAllRead({})).rejects.toThrow( + it('should throw error if neither workspaceId nor channelId provided', () => { + expect(() => client.markAllRead({})).toThrow( 'Either workspaceId or channelId is required', ) }) diff --git a/src/clients/threads-client.ts b/src/clients/threads-client.ts index a388d6e..22a12aa 100644 --- a/src/clients/threads-client.ts +++ b/src/clients/threads-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_THREADS, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { Thread, ThreadSchema, UnreadThread, UnreadThreadSchema } from '../types/entities' import { CreateThreadArgs, GetThreadsArgs, UpdateThreadArgs } from '../types/requests' @@ -26,6 +27,7 @@ export class ThreadsClient { * @param args.newer_than_ts - Optional timestamp to get threads newer than. * @param args.older_than_ts - Optional timestamp to get threads older than. * @param args.limit - Optional limit on number of threads returned. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of thread objects. * * @example @@ -34,34 +36,50 @@ export class ThreadsClient { * threads.forEach(t => console.log(t.title)) * ``` */ - async getThreads(args: GetThreadsArgs): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_THREADS}/get`, - this.apiToken, - args, - ) + getThreads(args: GetThreadsArgs, options: { batch: true }): BatchRequestDescriptor + getThreads(args: GetThreadsArgs, options?: { batch?: false }): Promise + getThreads( + args: GetThreadsArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_THREADS}/get` + const params = args + + if (options?.batch) { + return { method, url, params } + } - return response.data.map((thread) => ThreadSchema.parse(thread)) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data.map((thread) => ThreadSchema.parse(thread)), + ) } /** * Gets a single thread object by id. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The thread object. */ - async getThread(id: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_THREADS}/getone`, - this.apiToken, - { id }, - ) + getThread(id: number, options: { batch: true }): BatchRequestDescriptor + getThread(id: number, options?: { batch?: false }): Promise + getThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_THREADS}/getone` + const params = { id } + const schema = ThreadSchema - return ThreadSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -74,6 +92,7 @@ export class ThreadsClient { * @param args.recipients - Optional array of user IDs to notify. * @param args.attachments - Optional array of attachment objects. * @param args.sendAsIntegration - Optional flag to send as integration. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The created thread object. * * @example @@ -85,16 +104,24 @@ export class ThreadsClient { * }) * ``` */ - async createThread(args: CreateThreadArgs): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_THREADS}/add`, - this.apiToken, - args, - ) + createThread(args: CreateThreadArgs, options: { batch: true }): BatchRequestDescriptor + createThread(args: CreateThreadArgs, options?: { batch?: false }): Promise + createThread( + args: CreateThreadArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/add` + const params = args + const schema = ThreadSchema - return ThreadSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -106,91 +133,188 @@ export class ThreadsClient { * @param args.content - Optional new thread content. * @param args.recipients - Optional array of user IDs to notify. * @param args.attachments - Optional array of attachment objects. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated thread object. */ - async updateThread(args: UpdateThreadArgs): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_THREADS}/update`, - this.apiToken, - args, - ) + updateThread(args: UpdateThreadArgs, options: { batch: true }): BatchRequestDescriptor + updateThread(args: UpdateThreadArgs, options?: { batch?: false }): Promise + updateThread( + args: UpdateThreadArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/update` + const params = args + const schema = ThreadSchema - return ThreadSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Permanently deletes a thread. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async deleteThread(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/remove`, this.apiToken, { - id, - }) + deleteThread(id: number, options: { batch: true }): BatchRequestDescriptor + deleteThread(id: number, options?: { batch?: false }): Promise + deleteThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/remove` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Archives a thread. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async archiveThread(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/archive`, this.apiToken, { - id, - }) + archiveThread(id: number, options: { batch: true }): BatchRequestDescriptor + archiveThread(id: number, options?: { batch?: false }): Promise + archiveThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/archive` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Unarchives a thread. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async unarchiveThread(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/unarchive`, this.apiToken, { - id, - }) + unarchiveThread(id: number, options: { batch: true }): BatchRequestDescriptor + unarchiveThread(id: number, options?: { batch?: false }): Promise + unarchiveThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/unarchive` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Stars a thread. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async starThread(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/star`, this.apiToken, { id }) + starThread(id: number, options: { batch: true }): BatchRequestDescriptor + starThread(id: number, options?: { batch?: false }): Promise + starThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/star` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Unstars a thread. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async unstarThread(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/unstar`, this.apiToken, { - id, - }) + unstarThread(id: number, options: { batch: true }): BatchRequestDescriptor + unstarThread(id: number, options?: { batch?: false }): Promise + unstarThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/unstar` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Pins a thread. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async pinThread(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/pin`, this.apiToken, { id }) + pinThread(id: number, options: { batch: true }): BatchRequestDescriptor + pinThread(id: number, options?: { batch?: false }): Promise + pinThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/pin` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Unpins a thread. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async unpinThread(id: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/unpin`, this.apiToken, { - id, - }) + unpinThread(id: number, options: { batch: true }): BatchRequestDescriptor + unpinThread(id: number, options?: { batch?: false }): Promise + unpinThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/unpin` + const params = { id } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -198,15 +322,28 @@ export class ThreadsClient { * * @param id - The thread ID. * @param toChannel - The target channel ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async moveToChannel(id: number, toChannel: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_THREADS}/move_to_channel`, - this.apiToken, - { id, toChannel }, - ) + moveToChannel( + id: number, + toChannel: number, + options: { batch: true }, + ): BatchRequestDescriptor + moveToChannel(id: number, toChannel: number, options?: { batch?: false }): Promise + moveToChannel( + id: number, + toChannel: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/move_to_channel` + const params = { id, toChannel } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -214,12 +351,24 @@ export class ThreadsClient { * * @param id - The thread ID. * @param objIndex - The index of the last known read message. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async markRead(id: number, objIndex: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/mark_read`, this.apiToken, { - id, - obj_index: objIndex, - }) + markRead(id: number, objIndex: number, options: { batch: true }): BatchRequestDescriptor + markRead(id: number, objIndex: number, options?: { batch?: false }): Promise + markRead( + id: number, + objIndex: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/mark_read` + const params = { id, obj_index: objIndex } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -227,12 +376,24 @@ export class ThreadsClient { * * @param id - The thread ID. * @param objIndex - The index of the last unread message. Use -1 to mark the whole thread as unread. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async markUnread(id: number, objIndex: number): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_THREADS}/mark_unread`, this.apiToken, { - id, - obj_index: objIndex, - }) + markUnread(id: number, objIndex: number, options: { batch: true }): BatchRequestDescriptor + markUnread(id: number, objIndex: number, options?: { batch?: false }): Promise + markUnread( + id: number, + objIndex: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/mark_unread` + const params = { id, obj_index: objIndex } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -240,15 +401,28 @@ export class ThreadsClient { * * @param id - The thread ID. * @param objIndex - The index of the last unread message. Use -1 to mark the whole thread as unread. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async markUnreadForOthers(id: number, objIndex: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_THREADS}/mark_unread_for_others`, - this.apiToken, - { id, obj_index: objIndex }, - ) + markUnreadForOthers( + id: number, + objIndex: number, + options: { batch: true }, + ): BatchRequestDescriptor + markUnreadForOthers(id: number, objIndex: number, options?: { batch?: false }): Promise + markUnreadForOthers( + id: number, + objIndex: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/mark_unread_for_others` + const params = { id, obj_index: objIndex } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** @@ -257,6 +431,7 @@ export class ThreadsClient { * @param args - Either workspaceId or channelId (one is required). * @param args.workspaceId - The workspace ID. * @param args.channelId - The channel ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript @@ -267,7 +442,18 @@ export class ThreadsClient { * await api.threads.markAllRead({ channelId: 456 }) * ``` */ - async markAllRead(args: { workspaceId?: number; channelId?: number }): Promise { + markAllRead( + args: { workspaceId?: number; channelId?: number }, + options: { batch: true }, + ): BatchRequestDescriptor + markAllRead( + args: { workspaceId?: number; channelId?: number }, + options?: { batch?: false }, + ): Promise + markAllRead( + args: { workspaceId?: number; channelId?: number }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { if (!args.workspaceId && !args.channelId) { throw new Error('Either workspaceId or channelId is required') } @@ -276,46 +462,63 @@ export class ThreadsClient { if (args.workspaceId) params.workspace_id = args.workspaceId if (args.channelId) params.channel_id = args.channelId - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_THREADS}/mark_all_read`, - this.apiToken, - params, - ) + const method = 'POST' + const url = `${ENDPOINT_THREADS}/mark_all_read` + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Clears unread threads in a workspace. * * @param workspaceId - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async clearUnread(workspaceId: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_THREADS}/clear_unread`, - this.apiToken, - { workspace_id: workspaceId }, - ) + clearUnread(workspaceId: number, options: { batch: true }): BatchRequestDescriptor + clearUnread(workspaceId: number, options?: { batch?: false }): Promise + clearUnread( + workspaceId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/clear_unread` + const params = { workspace_id: workspaceId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Gets unread threads for a workspace. * * @param workspaceId - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Array of unread thread references. */ - async getUnread(workspaceId: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_THREADS}/get_unread`, - this.apiToken, - { workspace_id: workspaceId }, - ) + getUnread(workspaceId: number, options: { batch: true }): BatchRequestDescriptor + getUnread(workspaceId: number, options?: { batch?: false }): Promise + getUnread( + workspaceId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_THREADS}/get_unread` + const params = { workspace_id: workspaceId } + + if (options?.batch) { + return { method, url, params } + } - return response.data.map((thread) => UnreadThreadSchema.parse(thread)) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data.map((thread) => UnreadThreadSchema.parse(thread)), + ) } /** @@ -324,6 +527,7 @@ export class ThreadsClient { * * @param id - The thread ID. * @param minutes - Number of minutes to mute the thread. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated thread object. * * @example @@ -331,16 +535,29 @@ export class ThreadsClient { * const thread = await api.threads.muteThread(789, 30) * ``` */ - async muteThread(id: number, minutes: number): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_THREADS}/mute`, - this.apiToken, - { id, minutes }, - ) + muteThread( + id: number, + minutes: number, + options: { batch: true }, + ): BatchRequestDescriptor + muteThread(id: number, minutes: number, options?: { batch?: false }): Promise + muteThread( + id: number, + minutes: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/mute` + const params = { id, minutes } + const schema = ThreadSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return ThreadSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -348,17 +565,26 @@ export class ThreadsClient { * You will start to see notifications in your inbox again when new comments are added. * * @param id - The thread ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated thread object. */ - async unmuteThread(id: number): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_THREADS}/unmute`, - this.apiToken, - { id }, - ) + unmuteThread(id: number, options: { batch: true }): BatchRequestDescriptor + unmuteThread(id: number, options?: { batch?: false }): Promise + unmuteThread( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_THREADS}/unmute` + const params = { id } + const schema = ThreadSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return ThreadSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } } diff --git a/src/clients/users-client.ts b/src/clients/users-client.ts index 1806607..3afad1d 100644 --- a/src/clients/users-client.ts +++ b/src/clients/users-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_USERS, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { User, UserSchema } from '../types/entities' type AwayMode = { @@ -48,6 +49,7 @@ export class UsersClient { * @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). + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The authenticated user object. * * @example @@ -58,32 +60,50 @@ export class UsersClient { * }) * ``` */ - async login(args: { - email: string - password: string - setSessionCookie?: boolean - }): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_USERS}/login`, - undefined, - args, - ) + login( + args: { email: string; password: string; setSessionCookie?: boolean }, + options: { batch: true }, + ): BatchRequestDescriptor + login(args: { email: string; password: string; setSessionCookie?: boolean }): Promise + login( + args: { email: string; password: string; setSessionCookie?: boolean }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/login` + const params = args + const schema = UserSchema - return UserSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, undefined, params).then((response) => + schema.parse(response.data), + ) } /** * Logs out the current user and resets the session. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async logout(): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_USERS}/logout`, this.apiToken) + logout(options: { batch: true }): BatchRequestDescriptor + logout(options?: { batch?: false }): Promise + logout(options?: { batch?: boolean }): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/logout` + + if (options?.batch) { + return { method, url } + } + + return request(method, this.getBaseUri(), url, this.apiToken).then(() => undefined) } /** * Gets the associated user for the access token used in the request. * + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The authenticated user's information. * * @example @@ -92,21 +112,27 @@ export class UsersClient { * console.log(user.name, user.email) * ``` */ - async getSessionUser(): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_USERS}/get_session_user`, - this.apiToken, - ) + getSessionUser(options: { batch: true }): BatchRequestDescriptor + getSessionUser(options?: { batch?: false }): Promise + getSessionUser(options?: { batch?: boolean }): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_USERS}/get_session_user` + const schema = UserSchema - return UserSchema.parse(response.data) + if (options?.batch) { + return { method, url, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken).then((response) => + schema.parse(response.data), + ) } /** * Updates the logged-in user's details. * * @param args - The user properties to update. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated user object. * * @example @@ -117,39 +143,57 @@ export class UsersClient { * }) * ``` */ - async update(args: UpdateUserArgs): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_USERS}/update`, - this.apiToken, - args, - ) + update(args: UpdateUserArgs, options: { batch: true }): BatchRequestDescriptor + update(args: UpdateUserArgs, options?: { batch?: false }): Promise + update( + args: UpdateUserArgs, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/update` + const params = args + const schema = UserSchema - return UserSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Updates the user's password. * * @param newPassword - The new password. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated user object. */ - async updatePassword(newPassword: string): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_USERS}/update_password`, - this.apiToken, - { newPassword }, - ) + updatePassword(newPassword: string, options: { batch: true }): BatchRequestDescriptor + updatePassword(newPassword: string, options?: { batch?: false }): Promise + updatePassword( + newPassword: string, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/update_password` + const params = { newPassword } + const schema = UserSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return UserSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Invalidates the current API token and generates a new one. * + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The user object with the new token. * * @example @@ -158,26 +202,43 @@ export class UsersClient { * console.log('New token:', user.token) * ``` */ - async invalidateToken(): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_USERS}/invalidate_token`, - this.apiToken, - ) + invalidateToken(options: { batch: true }): BatchRequestDescriptor + invalidateToken(options?: { batch?: false }): Promise + invalidateToken(options?: { batch?: boolean }): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/invalidate_token` + const schema = UserSchema + + if (options?.batch) { + return { method, url, schema } + } - return UserSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken).then((response) => + schema.parse(response.data), + ) } /** * Validates a user token. * * @param token - The token to validate. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async validateToken(token: string): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_USERS}/validate_token`, undefined, { - token, - }) + validateToken(token: string, options: { batch: true }): BatchRequestDescriptor + validateToken(token: string, options?: { batch?: false }): Promise + validateToken( + token: string, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/validate_token` + const params = { token } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, undefined, params).then(() => undefined) } /** @@ -185,43 +246,83 @@ export class UsersClient { * * @param workspaceId - The workspace ID. * @param platform - The platform: 'mobile', 'desktop', or 'api'. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.users.heartbeat(123, 'api') * ``` */ - async heartbeat(workspaceId: number, platform: 'mobile' | 'desktop' | 'api'): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_USERS}/heartbeat`, this.apiToken, { - workspaceId, - platform, - }) + heartbeat( + workspaceId: number, + platform: 'mobile' | 'desktop' | 'api', + options: { batch: true }, + ): BatchRequestDescriptor + heartbeat( + workspaceId: number, + platform: 'mobile' | 'desktop' | 'api', + options?: { batch?: false }, + ): Promise + heartbeat( + workspaceId: number, + platform: 'mobile' | 'desktop' | 'api', + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/heartbeat` + const params = { workspaceId, platform } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Marks the user as inactive on a workspace (resets presence). * * @param workspaceId - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async resetPresence(workspaceId: number): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_USERS}/reset_presence`, - this.apiToken, - { workspaceId }, - ) + resetPresence(workspaceId: number, options: { batch: true }): BatchRequestDescriptor + resetPresence(workspaceId: number, options?: { batch?: false }): Promise + resetPresence( + workspaceId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/reset_presence` + const params = { workspaceId } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then(() => undefined) } /** * Sends a password reset email to the user. * * @param email - The user's email address. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async resetPassword(email: string): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_USERS}/reset_password`, undefined, { - email, - }) + resetPassword(email: string, options: { batch: true }): BatchRequestDescriptor + resetPassword(email: string, options?: { batch?: false }): Promise + resetPassword( + email: string, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/reset_password` + const params = { email } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, undefined, params).then(() => undefined) } /** @@ -229,46 +330,72 @@ export class UsersClient { * * @param resetCode - The reset code sent via email. * @param newPassword - The new password. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated user object. */ - async setPassword(resetCode: string, newPassword: string): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_USERS}/set_password`, - undefined, - { resetCode, newPassword }, - ) + setPassword( + resetCode: string, + newPassword: string, + options: { batch: true }, + ): BatchRequestDescriptor + setPassword(resetCode: string, newPassword: string, options?: { batch?: false }): Promise + setPassword( + resetCode: string, + newPassword: string, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/set_password` + const params = { resetCode, newPassword } + const schema = UserSchema - return UserSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, undefined, params).then((response) => + schema.parse(response.data), + ) } /** * Checks whether the user's account is connected to a Google account. * + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Google connection status. */ - async isConnectedToGoogle(): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_USERS}/is_connected_to_google`, - this.apiToken, - ) + isConnectedToGoogle(options: { batch: true }): BatchRequestDescriptor + isConnectedToGoogle(options?: { batch?: false }): Promise + isConnectedToGoogle(options?: { + batch?: boolean + }): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_USERS}/is_connected_to_google` - return response.data + if (options?.batch) { + return { method, url } + } + + return request(method, this.getBaseUri(), url, this.apiToken).then( + (response) => response.data, + ) } /** * Disconnects the user's account from their Google account. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async disconnectGoogle(): Promise { - await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_USERS}/disconnect_google`, - this.apiToken, - ) + disconnectGoogle(options: { batch: true }): BatchRequestDescriptor + disconnectGoogle(options?: { batch?: false }): Promise + disconnectGoogle(options?: { batch?: boolean }): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_USERS}/disconnect_google` + + if (options?.batch) { + return { method, url } + } + + return request(method, this.getBaseUri(), url, this.apiToken).then(() => undefined) } /** @@ -276,17 +403,32 @@ export class UsersClient { * Note: This does not delete the user's content as the content is owned by the workspace. * * @param password - The user's password for confirmation. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Status object with "ok" status. */ - async deleteUser(password: string): Promise<{ status: string }> { - const response = await request<{ status: string }>( - 'POST', + deleteUser( + password: string, + options: { batch: true }, + ): BatchRequestDescriptor<{ status: string }> + deleteUser(password: string, options?: { batch?: false }): Promise<{ status: string }> + deleteUser( + password: string, + options?: { batch?: boolean }, + ): Promise<{ status: string }> | BatchRequestDescriptor<{ status: string }> { + const method = 'POST' + const url = `${ENDPOINT_USERS}/delete` + const params = { password } + + if (options?.batch) { + return { method, url, params } + } + + return request<{ status: string }>( + method, this.getBaseUri(), - `${ENDPOINT_USERS}/delete`, + url, this.apiToken, - { password }, - ) - - return response.data + params, + ).then((response) => response.data) } } diff --git a/src/clients/workspace-users-client.ts b/src/clients/workspace-users-client.ts index 78bfefd..1739d60 100644 --- a/src/clients/workspace-users-client.ts +++ b/src/clients/workspace-users-client.ts @@ -21,6 +21,7 @@ export class WorkspaceUsersClient { * * @param workspaceId - The workspace ID. * @param archived - Optional flag to filter archived users. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of workspace user objects. * * @example @@ -29,34 +30,61 @@ export class WorkspaceUsersClient { * users.forEach(u => console.log(u.name, u.userType)) * ``` */ - async getWorkspaceUsers(workspaceId: number, archived?: boolean): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - 'workspace_users/get', - this.apiToken, - { id: workspaceId, archived }, - ) + getWorkspaceUsers( + workspaceId: number, + archived: boolean | undefined, + options: { batch: true }, + ): BatchRequestDescriptor + getWorkspaceUsers( + workspaceId: number, + archived?: boolean, + options?: { batch?: false }, + ): Promise + getWorkspaceUsers( + workspaceId: number, + archived?: boolean, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = 'workspace_users/get' + const params = { id: workspaceId, archived } - return response.data.map((user) => WorkspaceUserSchema.parse(user)) + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data.map((user) => WorkspaceUserSchema.parse(user)), + ) } /** * Returns a list of workspace user IDs for the given workspace id. * * @param workspaceId - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of user IDs. */ - async getWorkspaceUserIds(workspaceId: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - 'workspace_users/get_ids', - this.apiToken, - { id: workspaceId }, - ) + getWorkspaceUserIds( + workspaceId: number, + options: { batch: true }, + ): BatchRequestDescriptor + getWorkspaceUserIds(workspaceId: number, options?: { batch?: false }): Promise + getWorkspaceUserIds( + workspaceId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = 'workspace_users/get_ids' + const params = { id: workspaceId } - return response.data + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data, + ) } /** @@ -79,8 +107,16 @@ export class WorkspaceUsersClient { * const results = await batch.execute() * ``` */ - getUserById(workspaceId: number, userId: number, options: { batch: true }): BatchRequestDescriptor - getUserById(workspaceId: number, userId: number, options?: { batch?: false }): Promise + getUserById( + workspaceId: number, + userId: number, + options: { batch: true }, + ): BatchRequestDescriptor + getUserById( + workspaceId: number, + userId: number, + options?: { batch?: false }, + ): Promise getUserById( workspaceId: number, userId: number, @@ -95,8 +131,8 @@ export class WorkspaceUsersClient { return { method, url, params, schema } } - return request(method, this.getBaseUri(), url, this.apiToken, params).then((response) => - schema.parse(response.data), + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), ) } @@ -105,6 +141,7 @@ export class WorkspaceUsersClient { * * @param workspaceId - The workspace ID. * @param email - The user's email. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The workspace user object. * * @example @@ -112,16 +149,33 @@ export class WorkspaceUsersClient { * const user = await api.workspaceUsers.getUserByEmail(123, 'user@example.com') * ``` */ - async getUserByEmail(workspaceId: number, email: string): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - 'workspace_users/get_by_email', - this.apiToken, - { id: workspaceId, email }, - ) + getUserByEmail( + workspaceId: number, + email: string, + options: { batch: true }, + ): BatchRequestDescriptor + getUserByEmail( + workspaceId: number, + email: string, + options?: { batch?: false }, + ): Promise + getUserByEmail( + workspaceId: number, + email: string, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = 'workspace_users/get_by_email' + const params = { id: workspaceId, email } + const schema = WorkspaceUserSchema - return WorkspaceUserSchema.parse(response.data) + if (options?.batch) { + return { method, url, params, schema } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -129,18 +183,39 @@ export class WorkspaceUsersClient { * * @param workspaceId - The workspace ID. * @param userId - The user's ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns Information about the user in the workspace context. */ - async getUserInfo(workspaceId: number, userId: number): Promise> { - const response = await request>( - 'GET', + getUserInfo( + workspaceId: number, + userId: number, + options: { batch: true }, + ): BatchRequestDescriptor> + getUserInfo( + workspaceId: number, + userId: number, + options?: { batch?: false }, + ): Promise> + getUserInfo( + workspaceId: number, + userId: number, + options?: { batch?: boolean }, + ): Promise> | BatchRequestDescriptor> { + const method = 'GET' + const url = 'workspace_users/get_info' + const params = { id: workspaceId, user_id: userId } + + if (options?.batch) { + return { method, url, params } + } + + return request>( + method, this.getBaseUri(), - 'workspace_users/get_info', + url, this.apiToken, - { id: workspaceId, user_id: userId }, - ) - - return response.data + params, + ).then((response) => response.data) } /** @@ -148,6 +223,7 @@ export class WorkspaceUsersClient { * * @param workspaceId - The workspace ID. * @param userId - The user's ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The user's local time as a string (e.g., "2017-05-10 07:55:40"). * * @example @@ -156,16 +232,32 @@ export class WorkspaceUsersClient { * console.log('User local time:', localTime) * ``` */ - async getUserLocalTime(workspaceId: number, userId: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - 'workspace_users/get_local_time', - this.apiToken, - { id: workspaceId, user_id: userId }, - ) + getUserLocalTime( + workspaceId: number, + userId: number, + options: { batch: true }, + ): BatchRequestDescriptor + getUserLocalTime( + workspaceId: number, + userId: number, + options?: { batch?: false }, + ): Promise + getUserLocalTime( + workspaceId: number, + userId: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = 'workspace_users/get_local_time' + const params = { id: workspaceId, user_id: userId } - return response.data + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data, + ) } /** @@ -177,30 +269,55 @@ export class WorkspaceUsersClient { * @param args.name - Optional name for the user. * @param args.userType - Optional user type (USER, GUEST, or ADMIN). * @param args.channelIds - Optional array of channel IDs to add the user to. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The created workspace user object. */ - async addUser(args: { + addUser( + args: { + workspaceId: number + email: string + name?: string + userType?: UserType + channelIds?: number[] + }, + options: { batch: true }, + ): BatchRequestDescriptor + addUser(args: { workspaceId: number email: string name?: string userType?: UserType channelIds?: number[] - }): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - 'workspace_users/add', - this.apiToken, - { - id: args.workspaceId, - email: args.email, - name: args.name, - userType: args.userType, - channelIds: args.channelIds, - }, - ) + }): Promise + addUser( + args: { + workspaceId: number + email: string + name?: string + userType?: UserType + channelIds?: number[] + }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const params = { + id: args.workspaceId, + email: args.email, + name: args.name, + userType: args.userType, + channelIds: args.channelIds, + } + + const method = 'POST' + const url = 'workspace_users/add' + const schema = WorkspaceUserSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return WorkspaceUserSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -211,28 +328,51 @@ export class WorkspaceUsersClient { * @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). + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated workspace user object. */ - async updateUser(args: { + updateUser( + args: { + workspaceId: number + userType: UserType + email?: string + userId?: number + }, + options: { batch: true }, + ): BatchRequestDescriptor + updateUser(args: { workspaceId: number userType: UserType email?: string userId?: number - }): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - 'workspace_users/update', - this.apiToken, - { - id: args.workspaceId, - userType: args.userType, - email: args.email, - userId: args.userId, - }, - ) + }): Promise + updateUser( + args: { + workspaceId: number + userType: UserType + email?: string + userId?: number + }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const params = { + id: args.workspaceId, + userType: args.userType, + email: args.email, + userId: args.userId, + } + + const method = 'POST' + const url = 'workspace_users/update' + const schema = WorkspaceUserSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return WorkspaceUserSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -242,17 +382,41 @@ export class WorkspaceUsersClient { * @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). + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async removeUser(args: { - workspaceId: number - email?: string - userId?: number - }): Promise { - await request('POST', this.getBaseUri(), 'workspace_users/remove', this.apiToken, { + removeUser( + args: { + workspaceId: number + email?: string + userId?: number + }, + options: { batch: true }, + ): BatchRequestDescriptor + removeUser(args: { workspaceId: number; email?: string; userId?: number }): Promise + removeUser( + args: { + workspaceId: number + email?: string + userId?: number + }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const params = { id: args.workspaceId, email: args.email, userId: args.userId, - }) + } + + const method = 'POST' + const url = 'workspace_users/remove' + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** @@ -262,16 +426,40 @@ export class WorkspaceUsersClient { * @param args.workspaceId - The workspace ID. * @param args.email - The user's email. * @param args.userId - Optional user ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. */ - async resendInvite(args: { - workspaceId: number - email: string - userId?: number - }): Promise { - await request('POST', this.getBaseUri(), 'workspace_users/resend_invite', this.apiToken, { + resendInvite( + args: { + workspaceId: number + email: string + userId?: number + }, + options: { batch: true }, + ): BatchRequestDescriptor + resendInvite(args: { workspaceId: number; email: string; userId?: number }): Promise + resendInvite( + args: { + workspaceId: number + email: string + userId?: number + }, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const params = { id: args.workspaceId, email: args.email, userId: args.userId, - }) + } + + const method = 'POST' + const url = 'workspace_users/resend_invite' + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } } diff --git a/src/clients/workspaces-client.ts b/src/clients/workspaces-client.ts index 1dd7a5d..acda809 100644 --- a/src/clients/workspaces-client.ts +++ b/src/clients/workspaces-client.ts @@ -1,5 +1,6 @@ import { ENDPOINT_WORKSPACES, getTwistBaseUri } from '../consts/endpoints' import { request } from '../rest-client' +import type { BatchRequestDescriptor } from '../types/batch' import { Channel, ChannelSchema, Workspace, WorkspaceSchema } from '../types/entities' /** @@ -18,6 +19,7 @@ export class WorkspacesClient { /** * Gets all the user's workspaces. * + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of all workspaces the user belongs to. * * @example @@ -26,21 +28,28 @@ export class WorkspacesClient { * workspaces.forEach(ws => console.log(ws.name)) * ``` */ - async getWorkspaces(): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_WORKSPACES}/get`, - this.apiToken, - ) + getWorkspaces(options: { batch: true }): BatchRequestDescriptor + getWorkspaces(options?: { batch?: false }): Promise + getWorkspaces(options?: { + batch?: boolean + }): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_WORKSPACES}/get` + + if (options?.batch) { + return { method, url } + } - return response.data.map((workspace) => WorkspaceSchema.parse(workspace)) + return request(method, this.getBaseUri(), url, this.apiToken).then( + (response) => response.data.map((workspace) => WorkspaceSchema.parse(workspace)), + ) } /** * Gets a single workspace object by id. * * @param id - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The workspace object. * * @example @@ -49,21 +58,30 @@ export class WorkspacesClient { * console.log(workspace.name) * ``` */ - async getWorkspace(id: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_WORKSPACES}/getone`, - this.apiToken, - { id }, - ) + getWorkspace(id: number, options: { batch: true }): BatchRequestDescriptor + getWorkspace(id: number, options?: { batch?: false }): Promise + getWorkspace( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_WORKSPACES}/getone` + const params = { id } + const schema = WorkspaceSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return WorkspaceSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** * Gets the user's default workspace. * + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The default workspace object. * * @example @@ -72,15 +90,22 @@ export class WorkspacesClient { * console.log(workspace.name) * ``` */ - async getDefaultWorkspace(): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_WORKSPACES}/get_default`, - this.apiToken, - ) + getDefaultWorkspace(options: { batch: true }): BatchRequestDescriptor + getDefaultWorkspace(options?: { batch?: false }): Promise + getDefaultWorkspace(options?: { + batch?: boolean + }): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_WORKSPACES}/get_default` + const schema = WorkspaceSchema + + if (options?.batch) { + return { method, url, schema } + } - return WorkspaceSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken).then((response) => + schema.parse(response.data), + ) } /** @@ -88,6 +113,7 @@ export class WorkspacesClient { * * @param name - The name of the new workspace. * @param tempId - Optional temporary ID for the workspace. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The created workspace object. * * @example @@ -96,19 +122,31 @@ export class WorkspacesClient { * console.log('Created:', workspace.name) * ``` */ - async createWorkspace(name: string, tempId?: number): Promise { + createWorkspace( + name: string, + tempId: number | undefined, + options: { batch: true }, + ): BatchRequestDescriptor + createWorkspace(name: string, tempId?: number, options?: { batch?: false }): Promise + createWorkspace( + name: string, + tempId?: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { const params: Record = { name } if (tempId !== undefined) params.tempId = tempId - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_WORKSPACES}/add`, - this.apiToken, - params, - ) + const method = 'POST' + const url = `${ENDPOINT_WORKSPACES}/add` + const schema = WorkspaceSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return WorkspaceSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -116,6 +154,7 @@ export class WorkspacesClient { * * @param id - The workspace ID. * @param name - The new name for the workspace. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns The updated workspace object. * * @example @@ -123,16 +162,29 @@ export class WorkspacesClient { * const workspace = await api.workspaces.updateWorkspace(123, 'New Team Name') * ``` */ - async updateWorkspace(id: number, name: string): Promise { - const response = await request( - 'POST', - this.getBaseUri(), - `${ENDPOINT_WORKSPACES}/update`, - this.apiToken, - { id, name }, - ) + updateWorkspace( + id: number, + name: string, + options: { batch: true }, + ): BatchRequestDescriptor + updateWorkspace(id: number, name: string, options?: { batch?: false }): Promise + updateWorkspace( + id: number, + name: string, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_WORKSPACES}/update` + const params = { id, name } + const schema = WorkspaceSchema + + if (options?.batch) { + return { method, url, params, schema } + } - return WorkspaceSchema.parse(response.data) + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => schema.parse(response.data), + ) } /** @@ -140,23 +192,42 @@ export class WorkspacesClient { * * @param id - The workspace ID. * @param currentPassword - The user's current password for confirmation. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * * @example * ```typescript * await api.workspaces.removeWorkspace(123, 'mypassword') * ``` */ - async removeWorkspace(id: number, currentPassword: string): Promise { - await request('POST', this.getBaseUri(), `${ENDPOINT_WORKSPACES}/remove`, this.apiToken, { - id, - currentPassword, - }) + removeWorkspace( + id: number, + currentPassword: string, + options: { batch: true }, + ): BatchRequestDescriptor + removeWorkspace(id: number, currentPassword: string, options?: { batch?: false }): Promise + removeWorkspace( + id: number, + currentPassword: string, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'POST' + const url = `${ENDPOINT_WORKSPACES}/remove` + const params = { id, currentPassword } + + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + () => undefined, + ) } /** * Gets the public channels of a workspace. * * @param id - The workspace ID. + * @param options - Optional configuration. Set `batch: true` to return a descriptor for batch requests. * @returns An array of public channel objects. * * @example @@ -165,15 +236,22 @@ export class WorkspacesClient { * channels.forEach(ch => console.log(ch.name)) * ``` */ - async getPublicChannels(id: number): Promise { - const response = await request( - 'GET', - this.getBaseUri(), - `${ENDPOINT_WORKSPACES}/get_public_channels`, - this.apiToken, - { id }, - ) + getPublicChannels(id: number, options: { batch: true }): BatchRequestDescriptor + getPublicChannels(id: number, options?: { batch?: false }): Promise + getPublicChannels( + id: number, + options?: { batch?: boolean }, + ): Promise | BatchRequestDescriptor { + const method = 'GET' + const url = `${ENDPOINT_WORKSPACES}/get_public_channels` + const params = { id } - return response.data.map((channel) => ChannelSchema.parse(channel)) + if (options?.batch) { + return { method, url, params } + } + + return request(method, this.getBaseUri(), url, this.apiToken, params).then( + (response) => response.data.map((channel) => ChannelSchema.parse(channel)), + ) } } diff --git a/src/twist-api.ts b/src/twist-api.ts index b75a505..b49f1b9 100644 --- a/src/twist-api.ts +++ b/src/twist-api.ts @@ -11,6 +11,7 @@ import { ThreadsClient } from './clients/threads-client' import { UsersClient } from './clients/users-client' import { WorkspaceUsersClient } from './clients/workspace-users-client' import { WorkspacesClient } from './clients/workspaces-client' +import type { BatchRequestDescriptor, BatchResponseArray } from './types/batch' /** * The main API client for interacting with the Twist REST API. @@ -64,20 +65,24 @@ export class TwistApi { } /** - * Creates a batch builder for executing multiple API requests in a single HTTP call. + * Executes multiple API requests in a single HTTP call using the batch endpoint. * - * @returns A BatchBuilder instance + * @param requests - Batch request descriptors (obtained by passing `{ batch: true }` to API methods) + * @returns Array of batch responses with processed data * * @example * ```typescript - * const batch = api.createBatch() - * batch.add(() => api.workspaceUsers.getUserById(123, 456, { batch: true })) - * batch.add(() => api.workspaceUsers.getUserById(123, 789, { batch: true })) - * const results = await batch.execute() + * const results = await api.batch( + * api.workspaceUsers.getUserById(123, 456, { batch: true }), + * api.workspaceUsers.getUserById(123, 789, { batch: true }) + * ) * console.log(results[0].data.name, results[1].data.name) * ``` */ - createBatch(): BatchBuilder { - return new BatchBuilder(this.authToken, this.baseUrl) + batch[]>( + ...requests: T + ): Promise> { + const builder = new BatchBuilder(this.authToken, this.baseUrl) + return builder.execute(requests) } } diff --git a/src/types/batch.ts b/src/types/batch.ts index 84642cf..0548023 100644 --- a/src/types/batch.ts +++ b/src/types/batch.ts @@ -20,6 +20,14 @@ export type BatchResponse = { data: T } +/** + * Maps an array of BatchRequestDescriptor types to their corresponding BatchResponse types. + * Preserves individual types in heterogeneous arrays for proper type inference. + */ +export type BatchResponseArray[]> = { + [K in keyof T]: T[K] extends BatchRequestDescriptor ? BatchResponse : never +} + /** * Raw response format from the batch API endpoint. * @internal diff --git a/tsconfig.json b/tsconfig.json index 650024a..2301dd7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ "baseUrl": "./src" }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/testData", "src/**/*.test.ts", "src/testUtils/**/*"] + "exclude": ["node_modules", "dist", "**/testData"] } From 7e38bac90c4856d869ed60d9f285cedd538ecbec Mon Sep 17 00:00:00 2001 From: Scott Lovegrove Date: Sat, 11 Oct 2025 10:39:19 +0100 Subject: [PATCH 3/3] chore: Updates the readme --- README.md | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/README.md b/README.md index 040d186..3635db2 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,108 @@ const api = new TwistApi(tokenResponse.accessToken) const user = await api.users.getSessionUser() ``` +### Batch Requests + +The SDK supports making multiple API calls in a single HTTP request using the `/batch` endpoint. This can significantly improve performance when you need to fetch or update multiple resources. + +**Note:** Batch requests are completely optional. If you only need to make a single API call, simply call the method normally without the `{ batch: true }` option. + +#### How It Works + +To use batch requests: + +1. Pass `{ batch: true }` as the last parameter to any API method +2. This returns a `BatchRequestDescriptor` instead of executing the request immediately +3. Pass multiple descriptors to `api.batch()` to execute them together + +```typescript +// Single requests (normal usage) +const user1 = await api.workspaceUsers.getUserById(123, 456) +const user2 = await api.workspaceUsers.getUserById(123, 789) + +// Batch requests - executes in a single HTTP call +const results = await api.batch( + api.workspaceUsers.getUserById(123, 456, { batch: true }), + api.workspaceUsers.getUserById(123, 789, { batch: true }) +) + +console.log(results[0].data.name) // First user +console.log(results[1].data.name) // Second user +``` + +#### Response Structure + +Each item in the batch response includes: + +- `code` - HTTP status code for that specific request (e.g., 200, 404) +- `headers` - Response headers as a key-value object +- `data` - The parsed and validated response data + +```typescript +const results = await api.batch( + api.channels.getChannel(123, { batch: true }), + api.channels.getChannel(456, { batch: true }) +) + +results.forEach((result) => { + if (result.code === 200) { + console.log('Success:', result.data.name) + } else { + console.error('Error:', result.code) + } +}) +``` + +#### Performance Optimization + +When all requests in a batch are GET requests, they are executed in parallel on the server for optimal performance. Mixed GET and POST requests are executed sequentially. + +```typescript +// These GET requests execute in parallel +const results = await api.batch( + api.workspaceUsers.getUserById(123, 456, { batch: true }), + api.channels.getChannel(789, { batch: true }), + api.threads.getThread(101112, { batch: true }) +) +``` + +#### Mixing Different API Calls + +You can batch requests across different resource types: + +```typescript +const results = await api.batch( + api.workspaceUsers.getUserById(123, 456, { batch: true }), + api.channels.getChannels({ workspaceId: 123 }, { batch: true }), + api.conversations.getConversations({ workspaceId: 123 }, { batch: true }) +) + +const [user, channels, conversations] = results +// TypeScript maintains proper types for each result +console.log(user.data.name) +console.log(channels.data.length) +console.log(conversations.data.length) +``` + +#### Error Handling + +Individual requests in a batch can fail independently. Always check the status code of each result: + +```typescript +const results = await api.batch( + api.channels.getChannel(123, { batch: true }), + api.channels.getChannel(999999, { batch: true }) // Non-existent channel +) + +results.forEach((result, index) => { + if (result.code >= 200 && result.code < 300) { + console.log(`Request ${index} succeeded:`, result.data) + } else { + console.error(`Request ${index} failed with status ${result.code}`) + } +}) +``` + ## Documentation For detailed documentation, visit the [Twist SDK Documentation](https://doist.github.io/twist-sdk-typescript/).