basecamp-client 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -21
- package/dist/index.d.mts +447 -316
- package/dist/index.d.ts +447 -316
- package/dist/index.js +452 -382
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +452 -382
- package/dist/index.mjs.map +1 -1
- package/openapi.json +40007 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# basecamp-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript client for Basecamp 4 with typed requests and responses, automatic pagination, rate-limit retries, and OAuth token refresh.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,6 +10,28 @@ npm install basecamp-client
|
|
|
10
10
|
yarn add basecamp-client
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Both CommonJS (`require`) and ESM (`import`) entry points are included, along with full TypeScript declarations.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { buildClient, getBearerToken } from 'basecamp-client';
|
|
19
|
+
|
|
20
|
+
const bearerToken = await getBearerToken({
|
|
21
|
+
clientId: process.env.BASECAMP_CLIENT_ID!,
|
|
22
|
+
clientSecret: process.env.BASECAMP_CLIENT_SECRET!,
|
|
23
|
+
refreshToken: process.env.BASECAMP_REFRESH_TOKEN!,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const client = buildClient({
|
|
27
|
+
bearerToken,
|
|
28
|
+
accountId: process.env.BASECAMP_ACCOUNT_ID!,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { body: projects } = await client.projects.list({ query: {} });
|
|
32
|
+
console.log(projects);
|
|
33
|
+
```
|
|
34
|
+
|
|
13
35
|
## Usage
|
|
14
36
|
|
|
15
37
|
### Create a client
|
|
@@ -20,16 +42,36 @@ import { buildClient } from 'basecamp-client';
|
|
|
20
42
|
const client = buildClient({
|
|
21
43
|
bearerToken: process.env.BASECAMP_ACCESS_TOKEN!,
|
|
22
44
|
accountId: process.env.BASECAMP_ACCOUNT_ID!,
|
|
23
|
-
userAgent: process.env.BASECAMP_USER_AGENT!,
|
|
45
|
+
userAgent: process.env.BASECAMP_USER_AGENT!, // optional but recommended by Basecamp
|
|
24
46
|
});
|
|
25
47
|
|
|
26
|
-
const
|
|
27
|
-
|
|
48
|
+
const { status, body } = await client.projects.list({ query: {} });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`buildClient` creates a ts-rest client that targets `https://3.basecampapi.com/{accountId}`. It sets `Authorization`, `Accept`, and `Content-Type` headers automatically and appends `.json` to every request path.
|
|
52
|
+
|
|
53
|
+
### Refresh OAuth tokens
|
|
54
|
+
|
|
55
|
+
Basecamp access tokens expire. Use `getBearerToken` to exchange a refresh token for a fresh access token via the 37signals OAuth endpoint:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { getBearerToken } from 'basecamp-client';
|
|
59
|
+
|
|
60
|
+
const bearerToken = await getBearerToken({
|
|
61
|
+
clientId: process.env.BASECAMP_CLIENT_ID!,
|
|
62
|
+
clientSecret: process.env.BASECAMP_CLIENT_SECRET!,
|
|
63
|
+
refreshToken: process.env.BASECAMP_REFRESH_TOKEN!,
|
|
64
|
+
userAgent: process.env.BASECAMP_USER_AGENT!,
|
|
65
|
+
});
|
|
28
66
|
```
|
|
29
67
|
|
|
30
68
|
### Iterate through paginated endpoints
|
|
31
69
|
|
|
70
|
+
Basecamp collection endpoints return paginated results using `Link` headers. `asyncPagedIterator` follows those headers automatically:
|
|
71
|
+
|
|
32
72
|
```ts
|
|
73
|
+
import { asyncPagedIterator } from 'basecamp-client';
|
|
74
|
+
|
|
33
75
|
for await (const project of asyncPagedIterator({
|
|
34
76
|
fetchPage: client.projects.list,
|
|
35
77
|
request: { query: {} },
|
|
@@ -38,28 +80,173 @@ for await (const project of asyncPagedIterator({
|
|
|
38
80
|
}
|
|
39
81
|
```
|
|
40
82
|
|
|
41
|
-
|
|
83
|
+
To collect all pages into a single array:
|
|
42
84
|
|
|
43
|
-
|
|
85
|
+
```ts
|
|
86
|
+
import { asyncPagedToArray } from 'basecamp-client';
|
|
87
|
+
|
|
88
|
+
const allProjects = await asyncPagedToArray({
|
|
89
|
+
fetchPage: client.projects.list,
|
|
90
|
+
request: { query: {} },
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
|
|
96
|
+
| Option | Description |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `fetchPage` | The client method to call (e.g. `client.projects.list`). |
|
|
99
|
+
| `request` | Arguments forwarded to `fetchPage`. |
|
|
100
|
+
| `successStatus` | Expected HTTP status (default `200`). |
|
|
101
|
+
| `extractItems` | Custom function to pull items from the response (defaults to using `body` as an array). |
|
|
102
|
+
| `maxPages` | Stop after this many pages. |
|
|
103
|
+
|
|
104
|
+
## Supported resources
|
|
105
|
+
|
|
106
|
+
The client covers the following Basecamp 4 API resources. Each resource is accessed as a property on the client object (e.g. `client.projects`, `client.todos`).
|
|
107
|
+
|
|
108
|
+
### Projects and core
|
|
109
|
+
|
|
110
|
+
| Client property | Operations |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `projects` | list, get, create, update, trash |
|
|
113
|
+
| `people` | list, listForProject, listPingable, get, me, updateProjectAccess |
|
|
114
|
+
| `recordings` | list, trash, archive, activate |
|
|
115
|
+
| `events` | listForRecording |
|
|
116
|
+
| `lineupMarkers` | create, update, destroy |
|
|
117
|
+
|
|
118
|
+
### Messages and communication
|
|
119
|
+
|
|
120
|
+
| Client property | Operations |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `messageBoards` | get, listForProject |
|
|
123
|
+
| `messages` | list, get, create, update, pin, unpin, trash |
|
|
124
|
+
| `messageTypes` | list, get, create, update, destroy |
|
|
125
|
+
| `comments` | list, get, create, update, trash |
|
|
126
|
+
| `campfires` | list, get, listLines, getLine, createLine, deleteLine |
|
|
127
|
+
| `inboxes` | get |
|
|
128
|
+
| `forwards` | list, get, trash |
|
|
129
|
+
| `inboxReplies` | list, get |
|
|
130
|
+
| `clientCorrespondences` | list, get |
|
|
131
|
+
| `clientApprovals` | list, get |
|
|
132
|
+
| `clientReplies` | list, get |
|
|
133
|
+
| `clientVisibility` | update |
|
|
134
|
+
|
|
135
|
+
### To-dos
|
|
136
|
+
|
|
137
|
+
| Client property | Operations |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `todoSets` | get |
|
|
140
|
+
| `todoLists` | list, get, create, update, trash |
|
|
141
|
+
| `todoListGroups` | list, create, reposition |
|
|
142
|
+
| `todos` | list, get, create, update, complete, uncomplete, reposition, trash |
|
|
143
|
+
|
|
144
|
+
### Card tables (Kanban)
|
|
145
|
+
|
|
146
|
+
| Client property | Operations |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `cardTables` | get |
|
|
149
|
+
| `cardTableColumns` | get, create, update, move, watch, unwatch, enableOnHold, disableOnHold, updateColor |
|
|
150
|
+
| `cardTableCards` | list, get, create, update, move |
|
|
151
|
+
| `cardTableSteps` | create, update, setCompletion, reposition |
|
|
152
|
+
|
|
153
|
+
### Scheduling
|
|
154
|
+
|
|
155
|
+
| Client property | Operations |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `schedules` | get, update |
|
|
158
|
+
| `scheduleEntries` | list, get, getOccurrence, create, update, trash |
|
|
159
|
+
| `questionnaires` | get |
|
|
160
|
+
| `questions` | list, get |
|
|
161
|
+
|
|
162
|
+
### Documents and files
|
|
163
|
+
|
|
164
|
+
| Client property | Operations |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `vaults` | list, get, create, update, trash |
|
|
167
|
+
| `documents` | list, get, create, update, trash |
|
|
168
|
+
| `uploads` | list, get, create, update, trash |
|
|
169
|
+
| `attachments` | create |
|
|
170
|
+
|
|
171
|
+
## Conventions
|
|
172
|
+
|
|
173
|
+
### Path parameters
|
|
174
|
+
|
|
175
|
+
Most resource-scoped endpoints require a `bucketId` parameter (the Basecamp project ID) along with a resource-specific ID. All IDs are non-negative integers, coerced from path parameters via Zod.
|
|
44
176
|
|
|
45
177
|
```ts
|
|
46
|
-
|
|
178
|
+
// Fetch a single to-do
|
|
179
|
+
const { body: todo } = await client.todos.get({
|
|
180
|
+
params: { bucketId: 12345, todoId: 67890 },
|
|
181
|
+
});
|
|
182
|
+
```
|
|
47
183
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
184
|
+
### Recording-based status changes
|
|
185
|
+
|
|
186
|
+
Basecamp models many resources (messages, to-dos, documents, etc.) as "recordings". Trashing, archiving, and re-activating use a shared endpoint pattern through the `recordings` resource:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// Trash any recording
|
|
190
|
+
await client.recordings.trash({
|
|
191
|
+
params: { bucketId: 12345, recordingId: 67890 },
|
|
53
192
|
});
|
|
193
|
+
```
|
|
54
194
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
195
|
+
Individual resources also expose convenience `trash` operations that map to the same underlying endpoint.
|
|
196
|
+
|
|
197
|
+
### Querying and filtering
|
|
198
|
+
|
|
199
|
+
Collection endpoints accept query parameters for filtering and sorting:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const { body: todos } = await client.todos.list({
|
|
203
|
+
params: { bucketId: 12345, todolistId: 67890 },
|
|
204
|
+
query: { status: 'active', completed: 'true' },
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Common query parameters across resources:
|
|
209
|
+
|
|
210
|
+
| Parameter | Values |
|
|
211
|
+
| --- | --- |
|
|
212
|
+
| `status` | `active`, `archived`, `trashed` |
|
|
213
|
+
| `sort` | `created_at`, `updated_at` |
|
|
214
|
+
| `direction` | `asc`, `desc` |
|
|
215
|
+
| `page` | Page number (1-indexed) |
|
|
216
|
+
|
|
217
|
+
### Rate-limit handling
|
|
218
|
+
|
|
219
|
+
The built-in fetcher automatically retries on HTTP 429 responses (up to 20 attempts). It uses linear backoff with jitter and respects the `Retry-After` header when present. No configuration is needed.
|
|
220
|
+
|
|
221
|
+
### Error handling
|
|
222
|
+
|
|
223
|
+
The client is configured with `throwOnUnknownStatus: true`, meaning any response with a status code not declared in the contract will throw. Expected error statuses (404, 507, etc.) are part of the contract and returned as typed discriminated unions:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const response = await client.projects.get({
|
|
227
|
+
params: { projectId: 999 },
|
|
59
228
|
});
|
|
229
|
+
|
|
230
|
+
if (response.status === 404) {
|
|
231
|
+
console.log('Project not found');
|
|
232
|
+
} else {
|
|
233
|
+
console.log(response.body.name);
|
|
234
|
+
}
|
|
60
235
|
```
|
|
61
236
|
|
|
62
|
-
|
|
237
|
+
## The contract
|
|
238
|
+
|
|
239
|
+
Under the hood, every endpoint in this package is defined as a [ts-rest contract](https://ts-rest.com/). A contract is a declarative description of an API: its routes, path parameters, query parameters, request bodies, and response shapes -- all expressed with Zod schemas. The client you get from `buildClient` is generated directly from this contract, which is what makes every call fully type-safe with no code generation step.
|
|
240
|
+
|
|
241
|
+
Because the contract is a plain data structure (not tied to any HTTP library), it can be reused in ways that go beyond making API calls:
|
|
242
|
+
|
|
243
|
+
- **Custom fetchers** -- pass the contract to `initClient` from `@ts-rest/core` with your own fetch wrapper (e.g. to add logging, custom auth, or use a different HTTP library).
|
|
244
|
+
- **OpenAPI generation** -- the package already ships an `openapi.json` built from the contract. You can regenerate it or use the contract to produce docs, mock servers, or SDK stubs for other languages.
|
|
245
|
+
- **Server-side validation** -- if you build a Basecamp proxy or middleware, the same Zod schemas that type-check the client can validate incoming requests on the server.
|
|
246
|
+
- **Shared types** -- import the contract's inferred types into any TypeScript project so that producers and consumers of Basecamp data agree on the same shapes at compile time.
|
|
247
|
+
- **Runtime introspection** -- because the contract is a plain object with Zod schemas, you can iterate over its routes, inspect parameter and response schemas, or build tooling (e.g. CLI generators, permission auditors) that adapts automatically as the contract grows.
|
|
248
|
+
|
|
249
|
+
### Use the contract directly
|
|
63
250
|
|
|
64
251
|
```ts
|
|
65
252
|
import { contract } from 'basecamp-client';
|
|
@@ -68,6 +255,14 @@ import { initClient } from '@ts-rest/core';
|
|
|
68
255
|
const client = initClient(contract, { /* custom fetcher config */ });
|
|
69
256
|
```
|
|
70
257
|
|
|
258
|
+
### Type exports
|
|
259
|
+
|
|
260
|
+
The package exports the `Client` and `Contract` types, as well as Zod-inferred types for every schema:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import type { Client, Contract } from 'basecamp-client';
|
|
264
|
+
```
|
|
265
|
+
|
|
71
266
|
## Development
|
|
72
267
|
|
|
73
268
|
```bash
|
|
@@ -76,10 +271,10 @@ npm install
|
|
|
76
271
|
|
|
77
272
|
### Scripts
|
|
78
273
|
|
|
79
|
-
- `npm run build`
|
|
80
|
-
- `npm run contract:check`
|
|
81
|
-
- `npm test`
|
|
82
|
-
- `npm run format` / `npm run lint` / `npm run check`
|
|
274
|
+
- `npm run build` -- bundle the package with tsup (CJS + ESM + types).
|
|
275
|
+
- `npm run contract:check` -- type-check the contract with `tsc --noEmit`.
|
|
276
|
+
- `npm test` -- execute the Vitest live smoke suite *(requires Basecamp credentials and hits the real API)*.
|
|
277
|
+
- `npm run format` / `npm run lint` / `npm run check` -- Biome formatting and linting utilities.
|
|
83
278
|
|
|
84
279
|
### Environment variables
|
|
85
280
|
|
|
@@ -113,3 +308,7 @@ npm test
|
|
|
113
308
|
```bash
|
|
114
309
|
npm publish
|
|
115
310
|
```
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
MIT
|