Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down
186 changes: 186 additions & 0 deletions src/batch-builder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { HttpResponse, http } from 'msw'
import { beforeEach, describe, expect, it } from 'vitest'
import { server } from './testUtils/msw-setup'
import { TEST_API_TOKEN } from './testUtils/test-defaults'
import { TwistApi } from './twist-api'

describe('BatchBuilder', () => {
let api: TwistApi

beforeEach(() => {
api = new TwistApi(TEST_API_TOKEN)
})

describe('batch', () => {
it('should have a batch method', () => {
expect(typeof api.batch).toBe('function')
})
})

describe('add and execute', () => {
it('should batch multiple getUserById requests', async () => {
const mockUser1 = {
id: 456,
name: 'User One',
email: '[email protected]',
user_type: 'USER',
short_name: 'U1',
timezone: 'UTC',
removed: false,
bot: false,
version: 1,
}

const mockUser2 = {
id: 789,
name: 'User Two',
email: '[email protected]',
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 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)
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 results = await api.batch()
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: '[email protected]',
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 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)
// 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 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')
})
})

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: '[email protected]',
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')
})
})
})
Loading