Skip to content

Commit 788fab6

Browse files
feat: Adds support for making batch calls. (#25)
1 parent 5b45806 commit 788fab6

22 files changed

+2775
-838
lines changed

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,108 @@ const api = new TwistApi(tokenResponse.accessToken)
5454
const user = await api.users.getSessionUser()
5555
```
5656

57+
### Batch Requests
58+
59+
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.
60+
61+
**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.
62+
63+
#### How It Works
64+
65+
To use batch requests:
66+
67+
1. Pass `{ batch: true }` as the last parameter to any API method
68+
2. This returns a `BatchRequestDescriptor` instead of executing the request immediately
69+
3. Pass multiple descriptors to `api.batch()` to execute them together
70+
71+
```typescript
72+
// Single requests (normal usage)
73+
const user1 = await api.workspaceUsers.getUserById(123, 456)
74+
const user2 = await api.workspaceUsers.getUserById(123, 789)
75+
76+
// Batch requests - executes in a single HTTP call
77+
const results = await api.batch(
78+
api.workspaceUsers.getUserById(123, 456, { batch: true }),
79+
api.workspaceUsers.getUserById(123, 789, { batch: true })
80+
)
81+
82+
console.log(results[0].data.name) // First user
83+
console.log(results[1].data.name) // Second user
84+
```
85+
86+
#### Response Structure
87+
88+
Each item in the batch response includes:
89+
90+
- `code` - HTTP status code for that specific request (e.g., 200, 404)
91+
- `headers` - Response headers as a key-value object
92+
- `data` - The parsed and validated response data
93+
94+
```typescript
95+
const results = await api.batch(
96+
api.channels.getChannel(123, { batch: true }),
97+
api.channels.getChannel(456, { batch: true })
98+
)
99+
100+
results.forEach((result) => {
101+
if (result.code === 200) {
102+
console.log('Success:', result.data.name)
103+
} else {
104+
console.error('Error:', result.code)
105+
}
106+
})
107+
```
108+
109+
#### Performance Optimization
110+
111+
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.
112+
113+
```typescript
114+
// These GET requests execute in parallel
115+
const results = await api.batch(
116+
api.workspaceUsers.getUserById(123, 456, { batch: true }),
117+
api.channels.getChannel(789, { batch: true }),
118+
api.threads.getThread(101112, { batch: true })
119+
)
120+
```
121+
122+
#### Mixing Different API Calls
123+
124+
You can batch requests across different resource types:
125+
126+
```typescript
127+
const results = await api.batch(
128+
api.workspaceUsers.getUserById(123, 456, { batch: true }),
129+
api.channels.getChannels({ workspaceId: 123 }, { batch: true }),
130+
api.conversations.getConversations({ workspaceId: 123 }, { batch: true })
131+
)
132+
133+
const [user, channels, conversations] = results
134+
// TypeScript maintains proper types for each result
135+
console.log(user.data.name)
136+
console.log(channels.data.length)
137+
console.log(conversations.data.length)
138+
```
139+
140+
#### Error Handling
141+
142+
Individual requests in a batch can fail independently. Always check the status code of each result:
143+
144+
```typescript
145+
const results = await api.batch(
146+
api.channels.getChannel(123, { batch: true }),
147+
api.channels.getChannel(999999, { batch: true }) // Non-existent channel
148+
)
149+
150+
results.forEach((result, index) => {
151+
if (result.code >= 200 && result.code < 300) {
152+
console.log(`Request ${index} succeeded:`, result.data)
153+
} else {
154+
console.error(`Request ${index} failed with status ${result.code}`)
155+
}
156+
})
157+
```
158+
57159
## Documentation
58160

59161
For detailed documentation, visit the [Twist SDK Documentation](https://doist.github.io/twist-sdk-typescript/).

src/batch-builder.test.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { HttpResponse, http } from 'msw'
2+
import { beforeEach, describe, expect, it } from 'vitest'
3+
import { server } from './testUtils/msw-setup'
4+
import { TEST_API_TOKEN } from './testUtils/test-defaults'
5+
import { TwistApi } from './twist-api'
6+
7+
describe('BatchBuilder', () => {
8+
let api: TwistApi
9+
10+
beforeEach(() => {
11+
api = new TwistApi(TEST_API_TOKEN)
12+
})
13+
14+
describe('batch', () => {
15+
it('should have a batch method', () => {
16+
expect(typeof api.batch).toBe('function')
17+
})
18+
})
19+
20+
describe('add and execute', () => {
21+
it('should batch multiple getUserById requests', async () => {
22+
const mockUser1 = {
23+
id: 456,
24+
name: 'User One',
25+
26+
user_type: 'USER',
27+
short_name: 'U1',
28+
timezone: 'UTC',
29+
removed: false,
30+
bot: false,
31+
version: 1,
32+
}
33+
34+
const mockUser2 = {
35+
id: 789,
36+
name: 'User Two',
37+
38+
user_type: 'USER',
39+
short_name: 'U2',
40+
timezone: 'UTC',
41+
removed: false,
42+
bot: false,
43+
version: 1,
44+
}
45+
46+
server.use(
47+
http.post('https://api.twist.com/api/v3/batch', async ({ request }) => {
48+
const body = await request.text()
49+
const params = new URLSearchParams(body)
50+
const requestsStr = params.get('requests')
51+
const parallel = params.get('parallel')
52+
53+
expect(requestsStr).toBeDefined()
54+
expect(parallel).toBe('true') // All GET requests
55+
56+
const requests = JSON.parse(requestsStr!)
57+
expect(requests).toHaveLength(2)
58+
expect(requests[0].method).toBe('GET')
59+
expect(requests[0].url).toContain('workspace_users/getone')
60+
expect(requests[0].url).toContain('user_id=456')
61+
expect(requests[1].url).toContain('user_id=789')
62+
63+
return HttpResponse.json([
64+
{
65+
code: 200,
66+
headers: '',
67+
body: JSON.stringify(mockUser1),
68+
},
69+
{
70+
code: 200,
71+
headers: '',
72+
body: JSON.stringify(mockUser2),
73+
},
74+
])
75+
}),
76+
)
77+
78+
const results = await api.batch(
79+
api.workspaceUsers.getUserById(123, 456, { batch: true }),
80+
api.workspaceUsers.getUserById(123, 789, { batch: true }),
81+
)
82+
83+
expect(results).toHaveLength(2)
84+
expect(results[0].code).toBe(200)
85+
expect(results[0].data.id).toBe(456)
86+
expect(results[0].data.name).toBe('User One')
87+
expect(results[1].code).toBe(200)
88+
expect(results[1].data.id).toBe(789)
89+
expect(results[1].data.name).toBe('User Two')
90+
})
91+
92+
it('should handle empty batch', async () => {
93+
const results = await api.batch()
94+
expect(results).toEqual([])
95+
})
96+
97+
it('should handle error responses in batch', async () => {
98+
server.use(
99+
http.post('https://api.twist.com/api/v3/batch', async () => {
100+
return HttpResponse.json([
101+
{
102+
code: 200,
103+
headers: '',
104+
body: JSON.stringify({
105+
id: 456,
106+
name: 'User One',
107+
108+
user_type: 'USER',
109+
short_name: 'U1',
110+
timezone: 'UTC',
111+
removed: false,
112+
bot: false,
113+
version: 1,
114+
}),
115+
},
116+
{
117+
code: 404,
118+
headers: '',
119+
body: JSON.stringify({ error: 'User not found' }),
120+
},
121+
])
122+
}),
123+
)
124+
125+
const results = await api.batch(
126+
api.workspaceUsers.getUserById(123, 456, { batch: true }),
127+
api.workspaceUsers.getUserById(123, 999, { batch: true }),
128+
)
129+
130+
expect(results).toHaveLength(2)
131+
expect(results[0].code).toBe(200)
132+
expect(results[0].data.name).toBe('User One')
133+
expect(results[1].code).toBe(404)
134+
// Type assertion needed for error responses - error responses don't match the expected type
135+
expect((results[1].data as unknown as { error: string }).error).toBe('User not found')
136+
})
137+
138+
it('should accept array of descriptors', () => {
139+
const descriptors = [
140+
api.workspaceUsers.getUserById(123, 456, { batch: true }),
141+
api.workspaceUsers.getUserById(123, 789, { batch: true }),
142+
]
143+
expect(descriptors).toHaveLength(2)
144+
expect(descriptors[0].method).toBe('GET')
145+
expect(descriptors[0].url).toBe('workspace_users/getone')
146+
})
147+
})
148+
149+
describe('getUserById with batch option', () => {
150+
it('should return descriptor when batch: true', () => {
151+
const descriptor = api.workspaceUsers.getUserById(123, 456, { batch: true })
152+
153+
expect(descriptor).toEqual({
154+
method: 'GET',
155+
url: 'workspace_users/getone',
156+
params: { id: 123, user_id: 456 },
157+
schema: expect.any(Object), // WorkspaceUserSchema
158+
})
159+
})
160+
161+
it('should return promise when batch is not specified', async () => {
162+
server.use(
163+
http.get('https://api.twist.com/api/v4/workspace_users/getone', async () => {
164+
return HttpResponse.json({
165+
id: 456,
166+
name: 'User One',
167+
168+
user_type: 'USER',
169+
short_name: 'U1',
170+
timezone: 'UTC',
171+
removed: false,
172+
bot: false,
173+
version: 1,
174+
})
175+
}),
176+
)
177+
178+
const result = api.workspaceUsers.getUserById(123, 456)
179+
expect(result).toBeInstanceOf(Promise)
180+
181+
const user = await result
182+
expect(user.id).toBe(456)
183+
expect(user.name).toBe('User One')
184+
})
185+
})
186+
})

0 commit comments

Comments
 (0)