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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # basecamp-client
2
2
 
3
- Basecamp API client and contract built with `ts-rest`. The package exposes a fully typed contract, a ready-to-use client builder, and OAuth helpers so teams can share a single source of truth across services, CLIs, and tests.
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 projects = await client.projects.list({ query: {} });
27
- console.log(projects.body);
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
- `asyncPagedIterator` follows the `Link` response headers emitted by Basecamp collection routes, automatically fetching `page=2`, `page=3`, and beyond until the API signals the final page. The helper yields each item returned by the underlying endpoint or lets you define a custom `extractItems` function for non-array responses.
83
+ To collect all pages into a single array:
42
84
 
43
- ### Refresh OAuth tokens
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
- import { getBearerToken } from 'basecamp-client';
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
- const bearerToken = await getBearerToken({
49
- clientId: process.env.BASECAMP_CLIENT_ID!,
50
- clientSecret: process.env.BASECAMP_CLIENT_SECRET!,
51
- refreshToken: process.env.BASECAMP_REFRESH_TOKEN!,
52
- userAgent: process.env.BASECAMP_USER_AGENT!,
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
- const client = buildClient({
56
- bearerToken: bearerToken,
57
- accountId: process.env.BASECAMP_ACCOUNT_ID!,
58
- userAgent: process.env.BASECAMP_USER_AGENT!,
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
- ### Contract access
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` bundle the package with tsup (CJS + ESM + types).
80
- - `npm run contract:check` type-check the contract with `tsc --noEmit`.
81
- - `npm test` execute the Vitest live smoke suite *(requires Basecamp credentials and hits the real API)*.
82
- - `npm run format` / `npm run lint` / `npm run check` Biome formatting and linting utilities.
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