quickbase-js 2.3.0 → 2.4.1

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.
Files changed (62) hide show
  1. package/README.md +145 -1107
  2. package/dist/auth/ticket.d.ts.map +1 -1
  3. package/dist/auth/ticket.js +4 -3
  4. package/dist/auth/ticket.js.map +1 -1
  5. package/dist/client/request.d.ts +4 -0
  6. package/dist/client/request.d.ts.map +1 -1
  7. package/dist/client/request.js +15 -3
  8. package/dist/client/request.js.map +1 -1
  9. package/dist/generated/client.d.ts +2 -0
  10. package/dist/generated/client.d.ts.map +1 -1
  11. package/dist/generated/client.js +134 -113
  12. package/dist/generated/client.js.map +1 -1
  13. package/dist/generated/types.d.ts +2700 -2256
  14. package/dist/generated/types.d.ts.map +1 -1
  15. package/dist/generated/types.js +0 -2
  16. package/dist/generated/types.js.map +1 -1
  17. package/dist/quickbase.esm.js +3 -3
  18. package/dist/quickbase.esm.js.map +3 -3
  19. package/dist/quickbase.min.js +3 -3
  20. package/dist/quickbase.min.js.map +3 -3
  21. package/package.json +16 -13
  22. package/src/auth/index.ts +55 -0
  23. package/src/auth/sso-token.ts +143 -0
  24. package/src/auth/temp-token.ts +160 -0
  25. package/src/auth/ticket.ts +329 -0
  26. package/src/auth/token-cache.ts +97 -0
  27. package/src/auth/types.ts +50 -0
  28. package/src/auth/user-token.ts +38 -0
  29. package/src/cli/schema.ts +512 -0
  30. package/src/client/index.ts +36 -0
  31. package/src/client/pagination.ts +337 -0
  32. package/src/client/request.ts +544 -0
  33. package/src/client/retry.ts +169 -0
  34. package/src/client/throttle.ts +105 -0
  35. package/src/core/config.ts +160 -0
  36. package/src/core/dates.ts +63 -0
  37. package/src/core/errors.ts +349 -0
  38. package/src/core/index.ts +50 -0
  39. package/src/core/logger.ts +113 -0
  40. package/src/core/schema.ts +213 -0
  41. package/src/core/transform.ts +295 -0
  42. package/src/core/types.ts +337 -0
  43. package/src/generated/client.ts +1785 -0
  44. package/src/generated/operations.json +672 -0
  45. package/src/generated/types.ts +4884 -0
  46. package/src/index.ts +259 -0
  47. package/src/xml/codepages.ts +119 -0
  48. package/src/xml/dbvars.ts +75 -0
  49. package/src/xml/errors.ts +133 -0
  50. package/src/xml/fields.ts +121 -0
  51. package/src/xml/groups.ts +560 -0
  52. package/src/xml/html.ts +152 -0
  53. package/src/xml/index.ts +1196 -0
  54. package/src/xml/metadata.ts +111 -0
  55. package/src/xml/records.ts +301 -0
  56. package/src/xml/request.ts +238 -0
  57. package/src/xml/roles.ts +321 -0
  58. package/src/xml/schema.ts +436 -0
  59. package/src/xml/transform.ts +381 -0
  60. package/src/xml/types.ts +602 -0
  61. package/src/xml/users.ts +221 -0
  62. package/src/xml/webhooks.ts +260 -0
package/README.md CHANGED
@@ -1,1253 +1,291 @@
1
- # QuickBase JS SDK v2
1
+ # quickbase-js
2
2
 
3
- A TypeScript/JavaScript client for the QuickBase JSON RESTful API.
3
+ [![npm](https://img.shields.io/npm/v/quickbase-js)](https://www.npmjs.com/package/quickbase-js)
4
+ [![CI](https://github.com/DrewBradfordXYZ/quickbase-js/actions/workflows/ci.yml/badge.svg)](https://github.com/DrewBradfordXYZ/quickbase-js/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/quickbase-js)](LICENSE)
4
6
 
5
- ## Features
7
+ The QuickBase API for TypeScript and JavaScript, in Node.js, the browser and QuickBase code pages.
6
8
 
7
- - **Typed API Methods** - Full TypeScript support with auto-generated types from OpenAPI spec
8
- - **Multiple Auth Methods** - User token, temporary token, SSO, and ticket authentication
9
- - **Schema Aliases** - Use readable names for tables and fields instead of IDs
10
- - **Fluent Pagination** - Chain `.all()`, `.paginate()`, or `.noPaginate()` on paginated endpoints
11
- - **Date Conversion** - Automatic conversion of ISO date strings to JavaScript Date objects
12
- - **Rate Limit Handling** - Automatic retry with exponential backoff on 429 errors
13
- - **Proactive Throttling** - Optional client-side request throttling
14
- - **Tree-Shakeable** - Static method generation for optimal bundle size
15
- - **XML API Support** - Access legacy XML-only endpoints (roles, groups, DBVars, code pages, etc.)
16
- - **Read-Only Mode** - Defense-in-depth protection against accidental writes
17
-
18
- ## API Reference
19
-
20
- **[View Full API Documentation →](https://drewbradfordxyz.github.io/quickbase-js/)**
21
-
22
- The SDK provides typed methods for all 59 QuickBase API endpoints including Apps, Tables, Fields, Records, Reports, Users, and more. Each method includes JSDoc documentation with links to the official QuickBase API docs.
23
-
24
- ## Installation
9
+ ```typescript
10
+ const projects = await qb.runQuery({ from: 'bqtable1', where: "{7.EX.'Active'}" }).all();
11
+ ```
25
12
 
26
- ### npm / Node.js
13
+ - **Every endpoint** in QuickBase's API as a typed method, generated from QuickBase's published spec and checked against live responses.
14
+ - **Every page of results** with `.all()`, so you never write a `skip` loop.
15
+ - **Field names instead of IDs**: `select: ['name', 'status']`, with a generator that writes the names from your app.
16
+ - **No token in code pages**: the client signs in with the user's QuickBase session and renews its tokens.
17
+ - **Rate limits handled**: 429s and server errors are retried with backoff, and the client can pace itself to stay under the limit.
18
+ - **The XML API too**, for roles, groups, code pages, app variables and CSV imports.
19
+ - **No dependencies**, a read-only mode for tools that must never write, and typed errors.
27
20
 
28
21
  ```bash
29
22
  npm install quickbase-js
30
23
  ```
31
24
 
32
- ### Browser / CDN (QuickBase Code Pages)
33
-
34
- For QuickBase Code Pages, use temp-token authentication which automatically leverages the user's browser session:
35
-
36
- ```html
37
- <script src="https://cdn.jsdelivr.net/npm/quickbase-js@2/dist/quickbase.min.js"></script>
38
- <script>
39
- // Realm can be auto-detected from the current URL
40
- const realm = window.location.hostname.split('.')[0];
41
-
42
- const qb = QuickBase.createClient({
43
- realm: realm,
44
- auth: { type: 'temp-token' },
45
- });
46
-
47
- qb.getApp({ appId: 'bpqe82s1' }).then(app => {
48
- console.log(app.name);
49
- });
50
- </script>
51
- ```
52
-
53
- Or with ES modules:
54
-
55
- ```html
56
- <script type="module">
57
- import { createClient } from 'https://cdn.jsdelivr.net/npm/quickbase-js@2/dist/quickbase.esm.js';
58
-
59
- const qb = createClient({
60
- realm: 'mycompany',
61
- auth: { type: 'temp-token' },
62
- });
63
-
64
- const app = await qb.getApp({ appId: 'bpqe82s1' });
65
- console.log(app.name);
66
- </script>
67
- ```
25
+ ## Quick start
68
26
 
69
- ## Quick Start
27
+ Create a [user token](https://help.quickbase.com/docs/user-tokens) in QuickBase, then:
70
28
 
71
29
  ```typescript
72
30
  import { createClient } from 'quickbase-js';
73
31
 
74
32
  const qb = createClient({
75
- realm: 'mycompany',
76
- auth: {
77
- type: 'user-token',
78
- userToken: 'your-user-token',
79
- },
33
+ realm: 'mycompany', // mycompany.quickbase.com
34
+ auth: { type: 'user-token', userToken: process.env.QB_USER_TOKEN! },
80
35
  });
81
36
 
82
- // Use typed API methods
83
37
  const app = await qb.getApp({ appId: 'bpqe82s1' });
84
38
  console.log(app.name);
85
39
 
86
- // Query records
87
- const records = await qb.runQuery({
88
- from: 'bpqe82s1',
40
+ const result = await qb.runQuery({
41
+ from: 'bqtable1',
89
42
  select: [3, 6, 7],
90
- where: "{6.EX.'Active'}",
91
- });
92
- console.log(records.data);
93
- ```
94
-
95
- ## Authentication
96
-
97
- ### User Token (Recommended for Server-Side)
98
-
99
- ```typescript
100
- const qb = createClient({
101
- realm: 'mycompany',
102
- auth: {
103
- type: 'user-token',
104
- userToken: 'your-user-token',
105
- },
106
- });
107
- ```
108
-
109
- ### Temporary Token (Recommended for Browser/Code Pages)
110
-
111
- Temporary tokens are automatically fetched and cached per database. Tokens are refreshed proactively before expiration (default: 4m 50s TTL for 5-minute tokens). The SDK also handles unexpected 401 errors as a fallback.
112
-
113
- ```typescript
114
- // In QuickBase Code Pages - uses browser session for authentication
115
- const qb = createClient({
116
- realm: 'mycompany',
117
- auth: {
118
- type: 'temp-token',
119
- },
120
- });
121
- ```
122
-
123
- **Auto dbid extraction:** The SDK automatically extracts the database ID for temp token authentication from your request parameters:
124
-
125
- - `body.from` (runQuery, deleteRecords)
126
- - `body.to` (upsert)
127
- - `query.tableId` (getFields, etc.)
128
- - `query.appId` (getAppTables, etc.)
129
- - Path segments `/tables/{tableId}` and `/apps/{appId}`
130
-
131
- This means you don't need to manually specify the dbid - just make API calls and the SDK handles token management automatically.
132
-
133
- ### SSO Token
43
+ where: "{7.EX.'Active'}",
44
+ }).all(); // every page
45
+ for (const record of result.data ?? []) {
46
+ console.log(record['6']?.value);
47
+ }
134
48
 
135
- ```typescript
136
- const qb = createClient({
137
- realm: 'mycompany',
138
- auth: {
139
- type: 'sso',
140
- samlToken: 'your-saml-token',
141
- },
49
+ await qb.upsert({
50
+ to: 'bqtable1',
51
+ data: [{ '6': 'New project', '7': 'Active' }], // plain values are wrapped for you
142
52
  });
143
53
  ```
144
54
 
145
- ### Ticket Auth (Username/Password)
55
+ ## In a code page
146
56
 
147
- Ticket authentication lets users log in with their QuickBase email and password. Unlike user tokens, tickets properly attribute record changes (`createdBy`/`modifiedBy`) to the authenticated user. Credentials should come from user input (e.g., a login form), not hardcoded.
57
+ Load the browser bundle and use temporary-token auth. Calls run as the signed-in user, with that user's permissions, and there is no token in your page.
148
58
 
149
- ```typescript
150
- // Create client after user submits login form
151
- function handleLogin(formData: { username: string; password: string }) {
152
- const qb = createClient({
153
- realm: 'mycompany',
154
- auth: {
155
- type: 'ticket',
156
- username: formData.username,
157
- password: formData.password,
158
- hours: 24, // Optional: validity (default: 12h, max: ~6 months)
159
- persist: 'sessionStorage', // Optional: survive page refresh
160
- onExpired: () => { // Optional: called when ticket expires
161
- window.location.href = '/login';
162
- },
163
- },
59
+ ```html
60
+ <script src="https://cdn.jsdelivr.net/npm/quickbase-js@2/dist/quickbase.min.js"></script>
61
+ <script>
62
+ const qb = QuickBase.createClient({
63
+ realm: window.location.hostname.split('.')[0],
64
+ auth: { type: 'temp-token' },
164
65
  });
165
- return qb;
166
- }
66
+ qb.getApp({ appId: 'bpqe82s1' }).then((app) => console.log(app.name));
67
+ </script>
167
68
  ```
168
69
 
169
- **Key behaviors:**
170
- - Authentication happens lazily on the first API call
171
- - Password is discarded from memory immediately after authentication
172
- - Use `persist` to survive page refreshes (`'sessionStorage'` or `'localStorage'`)
173
- - Use `onExpired` to handle session expiration (e.g., redirect to login)
70
+ As a module: `import { createClient } from 'https://cdn.jsdelivr.net/npm/quickbase-js@2/dist/quickbase.esm.js'`.
174
71
 
175
- ## Pagination
72
+ [`examples/codepages`](examples/codepages) has a complete code page for each of vanilla web components, Alpine, Lit, Preact, Solid, React and Vue. Paste one into a new code page and it explores the app it's in.
176
73
 
177
- Paginated endpoints (`runQuery`, `runReport`, `getUsers`, etc.) return a `PaginatedRequest` that supports fluent pagination methods.
74
+ ## Common tasks
178
75
 
179
- ### Default Behavior (Single Page)
76
+ ### Get every record from a table
180
77
 
181
78
  ```typescript
182
- // Returns first page only
183
- const page = await qb.runQuery({ from: 'tableId' });
184
- console.log(page.data.length); // e.g., 100 records
185
- console.log(page.metadata.totalRecords); // e.g., 5000 total
79
+ const all = await qb.runQuery({ from: 'bqtable1' }).all();
80
+ const first500 = await qb.runQuery({ from: 'bqtable1' }).paginate({ limit: 500 });
81
+ const page = await qb.runQuery({ from: 'bqtable1' }); // one page
186
82
  ```
187
83
 
188
- ### Fetch All Pages
84
+ QuickBase returns a page at a time. `runQuery`, `runReport`, `getUsers` and the other paginated methods return one page when awaited, and follow the pages with `.all()` or `.paginate()`. Set `autoPaginate: true` on the client to make a plain `await` fetch them all.
189
85
 
190
- ```typescript
191
- // Automatically fetches all pages and combines results
192
- const all = await qb.runQuery({ from: 'tableId' }).all();
193
- console.log(all.data.length); // 5000 records
194
- ```
195
-
196
- ### Fetch with Record Limit
86
+ ### Filter and sort
197
87
 
198
88
  ```typescript
199
- // Fetch up to 500 records (may span multiple pages)
200
- const limited = await qb.runQuery({ from: 'tableId' }).paginate({ limit: 500 });
201
- console.log(limited.data.length); // 500 records
89
+ await qb.runQuery({
90
+ from: 'bqtable1',
91
+ select: [3, 6, 7],
92
+ where: '{6.GT.100}',
93
+ sortBy: [{ fieldId: 3, order: 'ASC' }],
94
+ options: { top: 100, skip: 0 },
95
+ });
202
96
  ```
203
97
 
204
- ### Explicit Single Page
98
+ `where` uses QuickBase's [query syntax](https://help.quickbase.com/docs/components-of-a-query): `{field.OPERATOR.'value'}`, joined with `AND` and `OR`.
205
99
 
206
- ```typescript
207
- // Explicitly fetch single page (useful when autoPaginate is enabled globally)
208
- const single = await qb.runQuery({ from: 'tableId' }).noPaginate();
209
- ```
210
-
211
- ### Global Auto-Pagination
100
+ ### Run a report
212
101
 
213
102
  ```typescript
214
- const qb = createClient({
215
- realm: 'mycompany',
216
- auth: { type: 'user-token', userToken: 'token' },
217
- autoPaginate: true, // Default await now fetches all pages
218
- });
219
-
220
- // Now direct await fetches all pages
221
- const all = await qb.runQuery({ from: 'tableId' });
222
-
223
- // Use .noPaginate() to get single page
224
- const page = await qb.runQuery({ from: 'tableId' }).noPaginate();
103
+ const reportRows = await qb.runReport({ reportId: '7', tableId: 'bqtable1' }).all();
225
104
  ```
226
105
 
227
- ## Date Conversion
106
+ ### Add or update records
228
107
 
229
- By default, ISO 8601 date strings in API responses are automatically converted to JavaScript `Date` objects:
108
+ `upsert` adds records, or updates the one whose merge field matches (the key field, usually Record ID#, when you don't name one):
230
109
 
231
110
  ```typescript
232
- const app = await qb.getApp({ appId: 'bpqe82s1' });
233
-
234
- // Date fields are now Date objects
235
- console.log(app.created instanceof Date); // true
236
- console.log(app.created.toISOString()); // "2024-01-15T10:30:00.000Z"
237
-
238
- // Works with nested objects and arrays
239
- const records = await qb.runQuery({ from: 'tableId' });
240
- records.data.forEach(record => {
241
- // Date field values are converted
242
- console.log(record['7'].value instanceof Date); // true for date/timestamp fields
111
+ const result = await qb.upsert({
112
+ to: 'bqtable1',
113
+ mergeFieldId: 9,
114
+ data: [{ '9': { value: 'EXT-001' }, '6': { value: 'Alpha' } }],
115
+ fieldsToReturn: [6],
243
116
  });
117
+ console.log(result.metadata?.createdRecordIds, result.metadata?.updatedRecordIds);
244
118
  ```
245
119
 
246
- To disable date conversion and keep dates as strings:
120
+ ### Delete records
247
121
 
248
122
  ```typescript
249
- const qb = createClient({
250
- realm: 'mycompany',
251
- auth: { type: 'user-token', userToken: 'token' },
252
- convertDates: false, // Keep dates as ISO strings
253
- });
123
+ await qb.deleteRecords({ from: 'bqtable1', where: '{3.EX.123}' });
254
124
  ```
255
125
 
256
- ## Schema Aliases
257
-
258
- Use readable names for tables and fields instead of cryptic IDs. The SDK transforms aliases to IDs in requests and IDs back to aliases in responses.
126
+ ### Use field names instead of IDs
259
127
 
260
- ### Defining a Schema
128
+ Give the client a schema and use your names anywhere an ID goes. Records come back keyed by name, with plain values:
261
129
 
262
130
  ```typescript
263
131
  const qb = createClient({
264
132
  realm: 'mycompany',
265
133
  auth: { type: 'user-token', userToken: 'token' },
266
- schema: {
267
- tables: {
268
- projects: {
269
- id: 'bqw3ryzab',
270
- fields: {
271
- id: 3,
272
- name: 6,
273
- status: 7,
274
- dueDate: 12,
275
- assignee: 15,
276
- },
277
- },
278
- tasks: {
279
- id: 'bqw4xyzcd',
280
- fields: {
281
- id: 3,
282
- title: 6,
283
- projectId: 8,
284
- completed: 10,
285
- },
286
- },
287
- },
288
- },
134
+ schema: { tables: { projects: { id: 'bqw3ryzab', fields: { id: 3, name: 6, status: 7 } } } },
289
135
  });
290
- ```
291
-
292
- ### Using Aliases in Queries
293
136
 
294
- ```typescript
295
- // Use table and field aliases instead of IDs
296
137
  const result = await qb.runQuery({
297
- from: 'projects', // Instead of 'bqw3ryzab'
298
- select: ['name', 'status', 'dueDate'], // Instead of [6, 7, 12]
299
- where: "{'status'.EX.'Active'}", // Field aliases in where clauses
300
- sortBy: [{ fieldId: 'dueDate', order: 'ASC' }],
301
- });
302
-
303
- // Response uses aliases and values are automatically unwrapped
304
- console.log(result.data[0].name); // "Project Alpha" (not { value: "Project Alpha" })
305
- console.log(result.data[0].status); // "Active"
306
- console.log(result.data[0].dueDate); // Date object (if convertDates is enabled)
307
- ```
308
-
309
- ### Upserting with Aliases
310
-
311
- With schema, use field aliases as keys:
312
-
313
- ```typescript
314
- await qb.upsert({
315
- to: 'projects',
316
- data: [
317
- { name: 'New Project', status: 'Planning' },
318
- ],
319
- });
320
- ```
321
-
322
- ## Auto-Wrapping
323
-
324
- Values are automatically wrapped in `{ value: X }` format for upserts. This works with or without a schema:
325
-
326
- ```typescript
327
- // Clean syntax - values are auto-wrapped
328
- { '6': 'New Record', '7': 42 }
329
-
330
- // Explicit wrapping also works (backwards compatible)
331
- { '6': { value: 'New Record' }, '7': { value: 42 } }
332
- ```
333
-
334
- ### Response Transformation
335
-
336
- Responses are automatically transformed:
337
- - Field ID keys (`'6'`) become aliases (`name`)
338
- - Values are unwrapped from `{ value: X }` to just `X`
339
- - Unknown fields (not in schema) keep their numeric key but are still unwrapped
340
-
341
- ```typescript
342
- // Raw API response:
343
- { data: [{ '6': { value: 'Alpha' }, '99': { value: 'Custom' } }] }
344
-
345
- // Transformed response (with schema):
346
- { data: [{ name: 'Alpha', '99': 'Custom' }] }
347
- ```
348
-
349
- ### Helpful Error Messages
350
-
351
- Typos in aliases throw errors with suggestions:
352
-
353
- ```typescript
354
- // Throws: Unknown field alias 'stauts' in table 'projects'. Did you mean 'status'?
355
- await qb.runQuery({
356
138
  from: 'projects',
357
- select: ['stauts'], // Typo!
139
+ select: ['name', 'status'],
140
+ where: "{'status'.EX.'Active'}",
358
141
  });
359
- ```
360
-
361
- ### Generating Schema from QuickBase
142
+ const first = result.data?.[0] as Record<string, unknown> | undefined; // TypeScript: keyed by name
143
+ console.log(first?.name, first?.status);
362
144
 
363
- Use the CLI to generate a schema from an existing app:
364
-
365
- ```bash
366
- # Generate TypeScript schema file
367
- npx quickbase-js schema -r "$QB_REALM" -a "$QB_APP_ID" -t "$QB_USER_TOKEN" -o schema.ts
368
-
369
- # Generate JSON format
370
- npx quickbase-js schema -r "$QB_REALM" -a "$QB_APP_ID" -t "$QB_USER_TOKEN" -f json -o schema.json
371
-
372
- # Output to stdout
373
- npx quickbase-js schema -r "$QB_REALM" -a "$QB_APP_ID" -t "$QB_USER_TOKEN"
145
+ await qb.upsert({ to: 'projects', data: [{ name: 'Alpha', status: 'Active' }] });
374
146
  ```
375
147
 
376
- The generator creates aliases from field labels using camelCase (e.g., "Due Date" → `dueDate`).
377
-
378
- ### Updating Schema with --merge
379
-
380
- When your QuickBase app changes (new tables, new fields), use `--merge` to update your schema while preserving any custom aliases you've set:
148
+ Generate the schema from your app instead of writing it, and regenerate with `--merge` when the app changes (your renamed aliases are kept):
381
149
 
382
150
  ```bash
383
- # Update schema, preserving custom aliases
384
- npx quickbase-js schema -r "$QB_REALM" -a "$QB_APP_ID" -t "$QB_USER_TOKEN" -o schema.ts --merge
151
+ npx quickbase-js schema -r mycompany -a bpqe82s1 -t "$QB_USER_TOKEN" -o schema.ts
385
152
  ```
386
153
 
387
- **What merge does:**
388
- - Preserves your custom table and field aliases (matched by ID, not name)
389
- - Adds new tables and fields with auto-generated aliases
390
- - Reports what changed:
154
+ A misspelled name throws with a suggestion (`Did you mean 'status'?`). Results are typed by field ID, which is why TypeScript needs the cast; the [schema guide](guides/schema-aliases.md) has the details.
391
155
 
392
- ```
393
- Merge complete:
394
- Tables: 2 preserved, 1 added, 0 removed
395
- Fields: 15 preserved, 3 added, 0 removed
396
- ```
397
-
398
- This lets you rename auto-generated aliases like `dateCreated` to `created` and keep them through updates.
399
-
400
- ### Using a Separate Schema File
401
-
402
- You can define your schema in a separate file and import it:
403
-
404
- **schema.ts**
405
- ```typescript
406
- import type { Schema } from 'quickbase-js';
407
-
408
- export const schema: Schema = {
409
- tables: {
410
- projects: {
411
- id: 'bqw3ryzab',
412
- fields: {
413
- id: 3,
414
- name: 6,
415
- status: 7,
416
- dueDate: 12,
417
- },
418
- },
419
- },
420
- };
421
- ```
156
+ ### Read an app's tables and fields
422
157
 
423
- **client.ts**
424
158
  ```typescript
425
- import { createClient } from 'quickbase-js';
426
- import { schema } from './schema.js';
159
+ const tables = await qb.getAppTables({ appId: 'bpqe82s1' });
160
+ console.log(tables.map((t) => `${t.id} ${t.name}`));
427
161
 
428
- const qb = createClient({
429
- realm: 'mycompany',
430
- auth: { type: 'user-token', userToken: 'token' },
431
- schema,
432
- });
162
+ const fields = await qb.getFields({ tableId: 'bqtable1' });
163
+ console.log(fields.map((f) => `${f.id} ${f.label} (${f.fieldType})`));
433
164
  ```
434
165
 
435
- Or use a JSON file:
166
+ ### Export and update a solution, download a file
436
167
 
437
- **schema.json**
438
- ```json
439
- {
440
- "tables": {
441
- "projects": {
442
- "id": "bqw3ryzab",
443
- "fields": { "id": 3, "name": 6, "status": 7 }
444
- }
445
- }
446
- }
447
- ```
168
+ Solutions are QBL (YAML) and files are raw content, so these take and return text:
448
169
 
449
170
  ```typescript
450
- import { createClient } from 'quickbase-js';
451
- import schema from './schema.json' assert { type: 'json' };
452
-
453
- const qb = createClient({
454
- realm: 'mycompany',
455
- auth: { type: 'user-token', userToken: 'token' },
456
- schema,
457
- });
458
- ```
459
-
460
- ## Configuration Options
461
-
462
- ```typescript
463
- const qb = createClient({
464
- // Required
465
- realm: 'mycompany',
466
- auth: { /* see authentication section */ },
467
-
468
- // Optional
469
- debug: false, // Enable debug logging
470
- timeout: 30000, // Request timeout in ms
471
- baseUrl: 'https://api.quickbase.com/v1', // API base URL
472
- autoPaginate: false, // Auto-paginate on direct await
473
- convertDates: true, // Convert ISO date strings to Date objects
474
- readOnly: false, // Block all write operations
475
- schema: { /* see schema aliases section */ }, // Table and field aliases
476
-
477
- // Rate limiting
478
- rateLimit: {
479
- // Optional client-side throttling
480
- // QuickBase allows 100 requests per 10 seconds per user token
481
- proactiveThrottle: {
482
- enabled: false,
483
- requestsPer10Seconds: 100,
484
- },
485
-
486
- // Retry configuration for 429 and 5xx errors
487
- retry: {
488
- maxAttempts: 3,
489
- initialDelayMs: 1000,
490
- maxDelayMs: 30000,
491
- multiplier: 2,
492
- },
493
-
494
- // Callback when rate limited
495
- onRateLimit: (info) => {
496
- console.log(`Rate limited! Retry after ${info.retryAfter}s`);
497
- console.log(`Request: ${info.requestUrl}`);
498
- console.log(`Ray ID: ${info.qbApiRay}`);
499
- },
500
- },
501
- });
171
+ const qbl = await qb.exportSolution({ solutionId: 'solution-id' });
172
+ await qb.updateSolution({ solutionId: 'solution-id' }, qbl);
173
+ const file = await qb.downloadFile({ tableId: 'bqtable1', recordId: 12, fieldId: 8, versionNumber: 0 }); // base64
502
174
  ```
503
175
 
504
- ## XML API (Legacy Endpoints)
176
+ ### Manage roles, groups and code pages
505
177
 
506
- The XML API provides access to endpoints not available in the JSON API, including role management, group management, app variables, code pages, and more.
507
-
508
- > **Note:** The XML API is a legacy API that may be discontinued by QuickBase in the future. Use the JSON API when possible.
509
-
510
- ### Quick Start
178
+ The JSON API doesn't cover these, so `createXmlClient(qb)` calls the XML API with the same authentication, retries, read-only mode and schema:
511
179
 
512
180
  ```typescript
513
- import { createClient, createXmlClient } from 'quickbase-js';
181
+ import { createXmlClient } from 'quickbase-js';
514
182
 
515
- // Create main client
516
- const qb = createClient({
517
- realm: 'mycompany',
518
- auth: { type: 'user-token', userToken: 'your-token' },
519
- });
520
-
521
- // Create XML client from main client
522
183
  const xml = createXmlClient(qb);
523
-
524
- // Get all roles in an app
525
184
  const roles = await xml.getRoleInfo(appId);
526
185
  for (const role of roles.roles) {
527
- console.log(`${role.name}: ${role.access.description}`);
528
- }
529
-
530
- // Get all users and their role assignments
531
- const users = await xml.userRoles(appId);
532
- for (const user of users.users) {
533
- console.log(`${user.name}: ${user.roles.map(r => r.name).join(', ')}`);
534
- }
535
-
536
- // Get comprehensive schema (fields, reports, variables)
537
- const schema = await xml.getSchema(tableId);
538
- for (const field of schema.table.fields ?? []) {
539
- console.log(`Field ${field.id}: ${field.label} (${field.fieldType})`);
540
- }
541
- ```
542
-
543
- ### Application Tokens
544
-
545
- Application tokens are an additional security layer for QuickBase apps. If an app has "Require Application Tokens" enabled, XML API calls must include a valid app token.
546
-
547
- **Important:** User tokens bypass app token checks entirely. You only need app tokens when:
548
- - Using ticket auth (`type: 'ticket'`) with the XML API
549
- - Using temp tokens (`type: 'temp-token'`) with the XML API
550
- - The target app has "Require Application Tokens" enabled
551
-
552
- The JSON API does not use app tokens.
553
-
554
- ```typescript
555
- // Set app token at client level
556
- const qb = createClient({
557
- realm: 'mycompany',
558
- auth: { type: 'ticket', username: 'user@example.com', password: 'pass' },
559
- appToken: 'your-app-token', // Used by XML API only
560
- });
561
-
562
- const xml = createXmlClient(qb);
563
- const roles = await xml.getRoleInfo(appId); // Includes app token
564
-
565
- // Or set it on the XML client directly
566
- const xmlWithToken = createXmlClient(qb, { appToken: 'your-app-token' });
567
- ```
568
-
569
- ### Available Methods
570
-
571
- **App Discovery:**
572
-
573
- | Method | XML Action | Description |
574
- |--------|------------|-------------|
575
- | `grantedDBs(opts?)` | API_GrantedDBs | List all apps/tables user can access |
576
- | `findDBByName(name, parentsOnly?)` | API_FindDBByName | Find an app by name |
577
- | `getDBInfo(dbid)` | API_GetDBInfo | Get app/table metadata (record count, manager, timestamps) |
578
- | `getNumRecords(tableId)` | API_GetNumRecords | Get total record count for a table |
579
-
580
- **Role Management:**
581
-
582
- | Method | XML Action | Description |
583
- |--------|------------|-------------|
584
- | `getRoleInfo(appId)` | API_GetRoleInfo | Get all roles defined in an application |
585
- | `userRoles(appId)` | API_UserRoles | Get all users and their role assignments |
586
- | `getUserRole(appId, userId, includeGroups?)` | API_GetUserRole | Get roles for a specific user |
587
- | `addUserToRole(appId, userId, roleId)` | API_AddUserToRole | Assign a user to a role |
588
- | `removeUserFromRole(appId, userId, roleId)` | API_RemoveUserFromRole | Remove a user from a role |
589
- | `changeUserRole(appId, userId, currentRole, newRole)` | API_ChangeUserRole | Change a user's role |
590
-
591
- **User Management:**
592
-
593
- | Method | XML Action | Description |
594
- |--------|------------|-------------|
595
- | `getUserInfo(email)` | API_GetUserInfo | Get user info by email address |
596
- | `provisionUser(appId, email, opts?)` | API_ProvisionUser | Create a new unregistered user |
597
- | `sendInvitation(appId, userId, userText?)` | API_SendInvitation | Send invitation email to a user |
598
- | `changeManager(appId, newManagerEmail)` | API_ChangeManager | Change the app manager |
599
- | `changeRecordOwner(tableId, recordId, newOwner)` | API_ChangeRecordOwner | Change record owner |
600
-
601
- **Group Management:**
602
-
603
- | Method | XML Action | Description |
604
- |--------|------------|-------------|
605
- | `createGroup(name, opts?)` | API_CreateGroup | Create a new group |
606
- | `deleteGroup(groupId)` | API_DeleteGroup | Delete a group |
607
- | `copyGroup(groupId, name, opts?)` | API_CopyGroup | Copy a group |
608
- | `changeGroupInfo(groupId, opts)` | API_ChangeGroupInfo | Update group name/description |
609
- | `getUsersInGroup(groupId, includeManagers?)` | API_GetUsersInGroup | Get users and managers in a group |
610
- | `addUserToGroup(groupId, userId, allowAdmin?)` | API_AddUserToGroup | Add a user to a group |
611
- | `removeUserFromGroup(groupId, userId)` | API_RemoveUserFromGroup | Remove a user from a group |
612
- | `getGroupRole(appId, groupId)` | API_GetGroupRole | Get roles assigned to a group |
613
- | `addGroupToRole(appId, groupId, roleId)` | API_AddGroupToRole | Assign a group to a role |
614
- | `removeGroupFromRole(appId, groupId, roleId, allRoles?)` | API_RemoveGroupFromRole | Remove a group from a role |
615
- | `grantedGroups(userId?, adminOnly?)` | API_GrantedGroups | Get groups a user belongs to |
616
- | `grantedDBsForGroup(groupId)` | API_GrantedDBsForGroup | Get apps a group can access |
617
-
618
- **App Metadata:**
619
-
620
- | Method | XML Action | Description |
621
- |--------|------------|-------------|
622
- | `getAppDTMInfo(appId)` | API_GetAppDTMInfo | Get modification timestamps |
623
- | `getAncestorInfo(appId)` | API_GetAncestorInfo | Get app copy/template lineage info |
624
-
625
- **Application Variables:**
626
-
627
- | Method | XML Action | Description |
628
- |--------|------------|-------------|
629
- | `getDBVar(appId, varName)` | API_GetDBVar | Get an application variable value |
630
- | `setDBVar(appId, varName, value)` | API_SetDBVar | Set an application variable value |
631
-
632
- **Code Pages:**
633
-
634
- | Method | XML Action | Description |
635
- |--------|------------|-------------|
636
- | `getDBPage(appId, pageIdOrName)` | API_GetDBPage | Get stored code page content |
637
- | `addReplaceDBPage(appId, opts)` | API_AddReplaceDBPage | Create or update a code page |
638
-
639
- **Field Management:**
640
-
641
- | Method | XML Action | Description |
642
- |--------|------------|-------------|
643
- | `fieldAddChoices(tableId, fieldId, choices)` | API_FieldAddChoices | Add choices to a multiple-choice field |
644
- | `fieldRemoveChoices(tableId, fieldId, choices)` | API_FieldRemoveChoices | Remove choices from a field |
645
- | `setKeyField(tableId, fieldId)` | API_SetKeyField | Set the key field for a table |
646
-
647
- **Schema Information:**
648
-
649
- | Method | XML Action | Description |
650
- |--------|------------|-------------|
651
- | `getSchema(dbid)` | API_GetSchema | Get comprehensive app/table metadata |
652
-
653
- **Record Information:**
654
-
655
- | Method | XML Action | Description |
656
- |--------|------------|-------------|
657
- | `doQueryCount(tableId, query?)` | API_DoQueryCount | Get count of matching records (no data fetch) |
658
- | `getRecordInfo(tableId, recordId)` | API_GetRecordInfo | Get record with field metadata |
659
- | `getRecordInfoByKey(tableId, keyValue)` | API_GetRecordInfo | Get record by key field value |
660
-
661
- **Record Operations:**
662
-
663
- | Method | XML Action | Description |
664
- |--------|------------|-------------|
665
- | `copyMasterDetail(tableId, opts)` | API_CopyMasterDetail | Copy a master record with its detail records |
666
- | `importFromCSV(tableId, opts)` | API_ImportFromCSV | Bulk import/update records from CSV data |
667
- | `runImport(tableId, importId)` | API_RunImport | Execute a saved import definition |
668
-
669
- **Webhooks:**
670
-
671
- | Method | XML Action | Description |
672
- |--------|------------|-------------|
673
- | `webhooksCreate(tableId, opts)` | API_Webhooks_Create | Create a webhook |
674
- | `webhooksEdit(tableId, webhookId, opts)` | API_Webhooks_Edit | Edit a webhook |
675
- | `webhooksDelete(tableId, webhookId)` | API_Webhooks_Delete | Delete a webhook |
676
- | `webhooksActivate(tableId, webhookId)` | API_Webhooks_Activate | Activate a webhook |
677
- | `webhooksDeactivate(tableId, webhookId)` | API_Webhooks_Deactivate | Deactivate a webhook |
678
- | `webhooksCopy(tableId, webhookId, name?)` | API_Webhooks_Copy | Copy a webhook |
679
-
680
- **HTML Generation:**
681
-
682
- | Method | XML Action | Description |
683
- |--------|------------|-------------|
684
- | `genAddRecordForm(tableId, fields?)` | API_GenAddRecordForm | Generate HTML form for adding a record |
685
- | `genResultsTable(tableId, opts?)` | API_GenResultsTable | Generate HTML/JS/CSV table of query results |
686
- | `getRecordAsHTML(tableId, opts)` | API_GetRecordAsHTML | Get a record rendered as HTML |
687
-
688
- **Authentication:**
689
-
690
- | Method | XML Action | Description |
691
- |--------|------------|-------------|
692
- | `signOut()` | API_SignOut | Clear ticket cookie (browser-focused) |
693
-
694
- ### Error Handling
695
-
696
- XML API errors are returned as `XmlError`:
697
-
698
- ```typescript
699
- import { XmlError, isUnauthorized, isNotFound, isInvalidTicket } from 'quickbase-js';
700
-
701
- try {
702
- const roles = await xml.getRoleInfo(appId);
703
- } catch (error) {
704
- if (error instanceof XmlError) {
705
- console.log(`XML API error ${error.code}: ${error.text}`);
706
-
707
- // Use helper functions for common error types
708
- if (isUnauthorized(error)) {
709
- console.log('Not authorized');
710
- } else if (isNotFound(error)) {
711
- console.log('Resource not found');
712
- } else if (isInvalidTicket(error)) {
713
- console.log('Invalid or expired ticket');
714
- }
715
- }
716
- }
717
- ```
718
-
719
- ### Read-Only Mode
720
-
721
- The XML client inherits read-only mode from the main client (see [Read-Only Mode](#read-only-mode-1)):
722
-
723
- ```typescript
724
- const qb = createClient({
725
- realm: 'mycompany',
726
- auth: { type: 'user-token', userToken: 'token' },
727
- readOnly: true,
728
- });
729
-
730
- const xml = createXmlClient(qb); // Inherits readOnly: true
731
-
732
- // Read operations work
733
- const roles = await xml.getRoleInfo(appId);
734
-
735
- // Write operations throw ReadOnlyError
736
- await xml.addUserToRole(appId, userId, roleId); // Throws!
737
- ```
738
-
739
- You can also override read-only mode for just the XML client:
740
-
741
- ```typescript
742
- const xml = createXmlClient(qb, { readOnly: true });
743
- ```
744
-
745
- ### Schema Aliases
746
-
747
- The XML client inherits the schema from the main client and supports table and field aliases:
748
-
749
- ```typescript
750
- const qb = createClient({
751
- realm: 'mycompany',
752
- auth: { type: 'user-token', userToken: 'token' },
753
- schema: {
754
- tables: {
755
- projects: {
756
- id: 'bqprojects123',
757
- fields: {
758
- id: 3,
759
- name: 6,
760
- status: 7,
761
- },
762
- },
763
- },
764
- },
765
- });
766
-
767
- const xml = createXmlClient(qb);
768
-
769
- // Use table aliases
770
- const count = await xml.getNumRecords('projects'); // Resolves to 'bqprojects123'
771
- const roles = await xml.getRoleInfo('projects');
772
-
773
- // Use field aliases
774
- await xml.fieldAddChoices('projects', 'status', ['Active', 'Inactive']);
775
- await xml.setKeyField('projects', 'name');
776
-
777
- // Field aliases in importFromCSV
778
- await xml.importFromCSV('projects', {
779
- recordsCsv: 'Project A,Active',
780
- clist: ['name', 'status'],
781
- mergeFieldId: 'id',
782
- });
783
- ```
784
-
785
- **Response Transformation:**
786
-
787
- The XML client transforms responses to use field and table aliases as keys when a schema is configured, similar to the JSON API:
788
-
789
- ```typescript
790
- // getRecordInfo - access fields by alias
791
- const record = await xml.getRecordInfo('projects', 123);
792
- console.log(record.fields.name.value); // "Project Alpha"
793
- console.log(record.fields.status.value); // "Active"
794
-
795
- // Without schema, fields are keyed by ID
796
- const record2 = await xmlNoSchema.getRecordInfo('bqprojects123', 123);
797
- console.log(record2.fields['6'].value); // "Project Alpha"
798
-
799
- // getSchema - access fields by alias
800
- const schema = await xml.getSchema('projects');
801
- console.log(schema.table.fields?.name.label); // "Project Name"
802
- console.log(schema.table.fields?.status.label); // "Status"
803
- console.log(schema.table.childTables?.tasks.dbid); // "bqtasks123"
804
-
805
- // grantedDBs - access databases by alias
806
- const dbs = await xml.grantedDBs();
807
- console.log(dbs.databases.projects.dbname); // "My Projects App"
808
- console.log(dbs.databases.tasks.dbname); // "Tasks Table"
809
-
810
- // Unknown tables/fields are keyed by their ID
811
- console.log(dbs.databases['bqunknown123'].dbname); // "Unknown Table"
812
-
813
- // getAppDTMInfo - access tables by alias
814
- const dtm = await xml.getAppDTMInfo('projects');
815
- console.log(dtm.appAlias); // "projects"
816
- console.log(dtm.tables.projects.lastModifiedTime); // "1234567890"
817
- console.log(dtm.tables.tasks.lastModifiedTime); // "1234567891"
818
- ```
819
-
820
- **Responses transformed to keyed objects:**
821
-
822
- | Method | Keyed Properties |
823
- |--------|-----------------|
824
- | `getRecordInfo` / `getRecordInfoByKey` | `fields.{alias}` |
825
- | `getSchema` | `table.fields.{alias}`, `table.childTables.{alias}` |
826
- | `grantedDBs` / `grantedDBsForGroup` | `databases.{alias}` |
827
- | `getAppDTMInfo` | `tables.{alias}` |
828
- | `fieldAddChoices` / `fieldRemoveChoices` | `fieldAlias` property |
829
- | `findDBByName` | `alias` property |
830
-
831
- ## API Methods
832
-
833
- All QuickBase API endpoints are available as typed methods:
834
-
835
- ```typescript
836
- // Apps
837
- const app = await qb.getApp({ appId: 'bpqe82s1' });
838
- const newApp = await qb.createApp({ name: 'My App', description: 'Test' });
839
- await qb.updateApp({ appId: 'bpqe82s1' }, { name: 'Updated Name' });
840
- await qb.deleteApp({ appId: 'bpqe82s1' }, { name: 'My App' });
841
-
842
- // Tables
843
- const tables = await qb.getAppTables({ appId: 'bpqe82s1' });
844
- const table = await qb.getTable({ appId: 'bpqe82s1', tableId: 'byyy82s1' });
845
-
846
- // Fields
847
- const fields = await qb.getFields({ tableId: 'byyy82s1' });
848
-
849
- // Records
850
- const records = await qb.runQuery({
851
- from: 'byyy82s1',
852
- select: [3, 6, 7],
853
- where: "{6.GT.100}",
854
- sortBy: [{ fieldId: 3, order: 'ASC' }],
855
- options: { top: 100, skip: 0 },
856
- });
857
-
858
- // Insert/Update records (values are auto-wrapped)
859
- const result = await qb.upsert({
860
- to: 'byyy82s1',
861
- data: [
862
- { '6': 'New Record', '7': 42 },
863
- ],
864
- });
865
-
866
- // Delete records
867
- await qb.deleteRecords({
868
- from: 'byyy82s1',
869
- where: "{3.EX.123}",
870
- });
871
-
872
- // Reports
873
- const report = await qb.runReport(
874
- { reportId: 'abc123', tableId: 'byyy82s1' },
875
- { skip: 0, top: 100 }
876
- );
877
-
878
- // Users
879
- const users = await qb.getUsers(
880
- { accountId: '123456' },
881
- { appIds: ['bpqe82s1'] }
882
- );
883
- ```
884
-
885
- ### Raw Request
886
-
887
- For endpoints not covered by typed methods, use the generic `request` method:
888
-
889
- ```typescript
890
- const result = await qb.request<MyResponseType>({
891
- method: 'POST',
892
- path: '/some/endpoint',
893
- body: { key: 'value' },
894
- query: { param: 'value' },
895
- });
896
- ```
897
-
898
- ## Read-Only Mode
899
-
900
- Enable read-only mode to prevent accidental writes during development, testing, or when building read-only dashboards. This feature uses defense-in-depth with two layers of protection.
901
-
902
- ```typescript
903
- import { createClient, createXmlClient, ReadOnlyError } from 'quickbase-js';
904
-
905
- const qb = createClient({
906
- realm: 'mycompany',
907
- auth: { type: 'user-token', userToken: 'token' },
908
- readOnly: true,
909
- });
910
-
911
- // Read operations work normally
912
- const app = await qb.getApp({ appId: 'bpqe82s1' });
913
- const records = await qb.runQuery({ from: 'byyy82s1' });
914
-
915
- // Write operations throw ReadOnlyError
916
- try {
917
- await qb.upsert({ to: 'byyy82s1', data: [{ '6': 'test' }] });
918
- } catch (error) {
919
- if (error instanceof ReadOnlyError) {
920
- console.log(`Blocked: ${error.method} ${error.path}`);
921
- // "Blocked: POST /records"
922
- }
923
- }
924
- ```
925
-
926
- ### What Gets Blocked
927
-
928
- | Operation Type | Examples | Blocked? |
929
- |----------------|----------|----------|
930
- | **GET requests** | getApp, getFields, getTables | ✅ Allowed |
931
- | **Query POSTs** | runQuery, runReport, getUsers | ✅ Allowed |
932
- | **Create/Update** | upsert, createApp, createTable | ❌ Blocked |
933
- | **Delete** | deleteRecords, deleteApp | ❌ Blocked |
934
- | **XML writes** | addUserToRole, setDBVar | ❌ Blocked |
935
-
936
- ### Defense-in-Depth
937
-
938
- The read-only check uses two layers:
939
-
940
- 1. **Explicit blocklist** - Known write endpoints are blocked by path matching
941
- 2. **HTTP method check** - Any POST/PUT/DELETE/PATCH not in the allowlist is blocked
942
-
943
- This ensures new API endpoints are blocked by default until explicitly allowlisted.
944
-
945
- ### XML API
946
-
947
- The XML client automatically inherits read-only mode from the main client:
948
-
949
- ```typescript
950
- const qb = createClient({
951
- realm: 'mycompany',
952
- auth: { type: 'user-token', userToken: 'token' },
953
- readOnly: true,
954
- });
955
-
956
- const xml = createXmlClient(qb); // Inherits readOnly: true
957
-
958
- // Read operations work
959
- const roles = await xml.getRoleInfo(appId);
960
-
961
- // Write operations throw ReadOnlyError with action info
962
- try {
963
- await xml.setDBVar(appId, 'myVar', 'value');
964
- } catch (error) {
965
- if (error instanceof ReadOnlyError) {
966
- console.log(`Blocked XML action: ${error.action}`);
967
- // "Blocked XML action: API_SetDBVar"
968
- }
186
+ console.log(role.name, role.access.description);
969
187
  }
970
188
  ```
971
189
 
972
- ### ReadOnlyError
973
-
974
- The `ReadOnlyError` class provides details about the blocked operation:
975
-
976
- ```typescript
977
- import { ReadOnlyError } from 'quickbase-js';
978
-
979
- try {
980
- await qb.deleteRecords({ from: 'byyy82s1', where: '{3.GT.0}' });
981
- } catch (error) {
982
- if (error instanceof ReadOnlyError) {
983
- error.method; // "DELETE"
984
- error.path; // "/records"
985
- error.action; // undefined (only set for XML API)
986
- error.message; // "Read-only mode: write operation blocked (DELETE /records)"
987
- }
988
- }
989
- ```
190
+ Every method is in the [XML API guide](guides/xml-api.md).
990
191
 
991
- ## Error Handling
192
+ ### Call any endpoint directly
992
193
 
993
194
  ```typescript
994
- import {
995
- QuickbaseError,
996
- RateLimitError,
997
- AuthenticationError,
998
- AuthorizationError,
999
- NotFoundError,
1000
- ValidationError,
1001
- ServerError,
1002
- QuickbaseTicketError,
1003
- QuickbaseAppTokenError,
1004
- QuickbaseXmlError,
1005
- ReadOnlyError,
1006
- } from 'quickbase-js';
1007
-
1008
- try {
1009
- await qb.getApp({ appId: 'invalid' });
1010
- } catch (error) {
1011
- if (error instanceof RateLimitError) {
1012
- console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
1013
- } else if (error instanceof AuthenticationError) {
1014
- console.log('Invalid or expired token');
1015
- } else if (error instanceof QuickbaseTicketError) {
1016
- console.log('XML Ticket expired or invalid');
1017
- } else if (error instanceof QuickbaseAppTokenError) {
1018
- console.log('Missing or invalid App Token');
1019
- } else if (error instanceof QuickbaseXmlError) {
1020
- console.log(`XML API error ${error.code}: ${error.text}`);
1021
- } else if (error instanceof AuthorizationError) {
1022
- console.log('Permission denied');
1023
- } else if (error instanceof NotFoundError) {
1024
- console.log('Resource not found');
1025
- } else if (error instanceof ValidationError) {
1026
- console.log('Bad request:', error.message);
1027
- } else if (error instanceof ServerError) {
1028
- console.log('Server error:', error.statusCode);
1029
- } else if (error instanceof QuickbaseError) {
1030
- console.log(`API error ${error.statusCode}: ${error.message}`);
1031
- }
1032
- }
195
+ const result = await qb.request<{ id: string }>({ method: 'GET', path: '/apps/bpqe82s1' });
1033
196
  ```
1034
197
 
1035
- ## Rate Limiting
1036
-
1037
- QuickBase enforces a rate limit of **100 requests per 10 seconds** per user token. This SDK follows [QuickBase's official rate limiting guidance](https://developer.quickbase.com/rateLimit) — relying on server-side `Retry-After` headers by default, with optional client-side throttling.
198
+ `request` uses the client's authentication, retries and throttling.
1038
199
 
1039
- ### How 429 Errors Are Handled
200
+ ## Authentication
1040
201
 
1041
- When the SDK receives a 429 (Too Many Requests) response, it automatically:
202
+ | `auth.type` | Use it for |
203
+ |---|---|
204
+ | `'user-token'` | Servers and scripts |
205
+ | `'temp-token'` | Code pages and other browser code inside QuickBase |
206
+ | `'ticket'` | A user signing in with their QuickBase email and password |
207
+ | `'sso'` | A user signed in through your identity provider (SAML) |
1042
208
 
1043
- 1. **Extracts rate limit info** from response headers (`Retry-After`, `cf-ray`, `qb-api-ray`)
1044
- 2. **Calls the `onRateLimit` callback** if configured, allowing you to log or monitor
1045
- 3. **Waits before retrying** - uses the `Retry-After` header if present, otherwise exponential backoff with jitter
1046
- 4. **Retries the request** up to `maxAttempts` times (default: 3)
1047
- 5. **Throws a `RateLimitError`** if all retries are exhausted
209
+ Details are in the [authentication guide](guides/authentication.md).
1048
210
 
1049
- ```
1050
- Request fails with 429
1051
- ↓
1052
- Extract Retry-After header
1053
- ↓
1054
- Call onRateLimit callback (if set)
1055
- ↓
1056
- Wait (Retry-After or exponential backoff)
1057
- ↓
1058
- Retry request (up to maxAttempts)
1059
- ↓
1060
- Throw RateLimitError if exhausted
1061
- ```
211
+ ## Rate limits and retries
1062
212
 
1063
- ### Retry Configuration
213
+ QuickBase allows 100 requests per 10 seconds per user token. The client retries 429s and server errors with exponential backoff, honoring `Retry-After`, and can throttle itself so it never hits the limit:
1064
214
 
1065
215
  ```typescript
1066
- const qb = createClient({
216
+ createClient({
1067
217
  realm: 'mycompany',
1068
218
  auth: { type: 'user-token', userToken: 'token' },
1069
219
  rateLimit: {
1070
- retry: {
1071
- maxAttempts: 5, // Default: 3
1072
- initialDelayMs: 1000, // Default: 1000ms
1073
- maxDelayMs: 30000, // Default: 30000ms
1074
- multiplier: 2, // Default: 2
1075
- },
220
+ proactiveThrottle: { enabled: true, requestsPer10Seconds: 100 },
221
+ retry: { maxAttempts: 5 },
1076
222
  },
1077
223
  });
1078
224
  ```
1079
225
 
1080
- The backoff formula with jitter: `delay = initialDelayMs * (multiplier ^ attempt) ± 10%`
226
+ Timeouts, backoff settings and a rate-limit callback are in the [reliability guide](guides/reliability.md).
1081
227
 
1082
- ### Proactive Throttling
228
+ ## Errors
1083
229
 
1084
- Prevent 429 errors entirely by throttling requests client-side using a sliding window algorithm:
230
+ Every error extends `QuickbaseError`, with a class for each kind of failure:
1085
231
 
1086
232
  ```typescript
1087
- const qb = createClient({
1088
- realm: 'mycompany',
1089
- auth: { type: 'user-token', userToken: 'token' },
1090
- rateLimit: {
1091
- proactiveThrottle: {
1092
- enabled: true,
1093
- requestsPer10Seconds: 100,
1094
- },
1095
- },
1096
- });
1097
- ```
1098
-
1099
- This tracks request timestamps and blocks new requests when the limit would be exceeded, waiting until the oldest request exits the 10-second window.
1100
-
1101
- ### Rate Limit Callback
1102
-
1103
- Get notified when rate limited (called before retry):
1104
-
1105
- ```typescript
1106
- const qb = createClient({
1107
- realm: 'mycompany',
1108
- auth: { type: 'user-token', userToken: 'token' },
1109
- rateLimit: {
1110
- onRateLimit: (info) => {
1111
- console.log(`Rate limited on ${info.requestUrl}`);
1112
- console.log(`Retry after: ${info.retryAfter} seconds`);
1113
- console.log(`Ray ID: ${info.qbApiRay}`);
1114
- console.log(`Attempt: ${info.attempt}`);
1115
- },
1116
- },
1117
- });
1118
- ```
1119
-
1120
- ### Handling RateLimitError
1121
-
1122
- If retries are exhausted, a `RateLimitError` is thrown:
1123
-
1124
- ```typescript
1125
- import { RateLimitError } from 'quickbase-js';
233
+ import { RateLimitError, NotFoundError, ReadOnlyError, QuickbaseError } from 'quickbase-js';
1126
234
 
1127
235
  try {
1128
236
  await qb.getApp({ appId: 'bpqe82s1' });
1129
237
  } catch (error) {
1130
238
  if (error instanceof RateLimitError) {
1131
- console.log(`Rate limited after ${error.rateLimitInfo.attempt} attempts`);
1132
- console.log(`Retry after: ${error.retryAfter} seconds`);
239
+ console.log(`rate limited; retry after ${error.retryAfter}s`);
240
+ } else if (error instanceof NotFoundError) {
241
+ console.log('no such app');
242
+ } else if (error instanceof ReadOnlyError) {
243
+ console.log(`write blocked: ${error.method} ${error.path}`);
244
+ } else if (error instanceof QuickbaseError) {
245
+ console.log(`QuickBase error ${error.statusCode}: ${error.message}`);
1133
246
  }
1134
247
  }
1135
248
  ```
1136
249
 
1137
- ## Migration from v1
250
+ | Class | Status |
251
+ |---|---|
252
+ | `ValidationError` | 400 |
253
+ | `AuthenticationError` | 401 |
254
+ | `AuthorizationError` | 403 |
255
+ | `NotFoundError` | 404 |
256
+ | `RateLimitError` | 429 |
257
+ | `ServerError` | 5xx |
258
+ | `ReadOnlyError` | a write blocked by `readOnly: true` |
1138
259
 
1139
- ### Authentication
260
+ The XML API adds `QuickbaseTicketError`, `QuickbaseAppTokenError` and `QuickbaseXmlError`.
1140
261
 
1141
- ```typescript
1142
- // v1 - user token
1143
- const qb = quickbase({ realm: 'company', userToken: 'token' });
262
+ ## Other options
1144
263
 
1145
- // v2 - user token (explicit type)
1146
- const qb = createClient({
1147
- realm: 'company',
1148
- auth: { type: 'user-token', userToken: 'token' },
1149
- });
264
+ - **`readOnly: true`** refuses every write before it is sent, in both APIs. Reads sent as POST (`runQuery`, `runReport`, `audit`) still work.
265
+ - **Dates**: ISO date strings in responses become `Date` objects. The types say `string`, because QuickBase's spec marks few date fields; set `convertDates: false` to keep strings.
266
+ - **Requirements**: Node.js 18 or later, or any modern browser. The package is ES modules; the browser bundle also defines a `QuickBase` global.
1150
267
 
1151
- // v1 - temp tokens in Code Pages
1152
- const qb = quickbase({ realm: 'company', useTempTokens: true });
268
+ ## Documentation
1153
269
 
1154
- // v2 - temp tokens in Code Pages
1155
- const qb = createClient({
1156
- realm: 'company',
1157
- auth: { type: 'temp-token' },
1158
- });
1159
- ```
1160
-
1161
- ### Pagination
1162
-
1163
- ```typescript
1164
- // v1 - callback-based
1165
- await qb.withPaginationDisabled(async () => qb.runQuery({ body: { from: tableId } }));
1166
- await qb.withPaginationLimit(100, async () => qb.runQuery({ body: { from: tableId } }));
1167
-
1168
- // v2 - fluent API
1169
- await qb.runQuery({ from: tableId }).noPaginate();
1170
- await qb.runQuery({ from: tableId }).paginate({ limit: 100 });
1171
- ```
1172
-
1173
- ### Request Body
1174
-
1175
- ```typescript
1176
- // v1 - body wrapper
1177
- await qb.runQuery({ body: { from: tableId, select: [3, 6] } });
1178
-
1179
- // v2 - direct parameters
1180
- await qb.runQuery({ from: tableId, select: [3, 6] });
1181
- ```
270
+ - Guides: [queries and pagination](guides/queries.md), [schema aliases](guides/schema-aliases.md), [authentication](guides/authentication.md), [rate limits and retries](guides/reliability.md), [the XML API](guides/xml-api.md), [migrating from v1](guides/migration.md)
271
+ - [API reference](https://drewbradfordxyz.github.io/quickbase-js/): every method, with QuickBase's descriptions
272
+ - [CHANGELOG](CHANGELOG.md)
1182
273
 
1183
274
  ## Development
1184
275
 
1185
276
  ```bash
1186
- # Clone with submodules (includes OpenAPI spec)
1187
277
  git clone --recurse-submodules https://github.com/DrewBradfordXYZ/quickbase-js.git
1188
-
1189
- # Or if already cloned, initialize submodules
1190
- git submodule update --init
1191
-
1192
- # Install dependencies
1193
278
  npm install
1194
-
1195
- # Run tests
1196
- npm test
1197
-
1198
- # Build
1199
- npm run build
1200
-
1201
- # Type check
1202
- npm run typecheck
1203
-
1204
- # Generate types from OpenAPI spec
1205
- npm run spec:generate
279
+ npm test # unit tests
280
+ npm run typecheck # the library, and the docs' examples
281
+ npm run spec:build # regenerate the types from the pinned spec in spec/
1206
282
  ```
1207
283
 
1208
- ### Updating the OpenAPI Spec
1209
-
1210
- The `spec/` directory is a Git submodule pointing to [quickbase-spec](https://github.com/DrewBradfordXYZ/quickbase-spec). Each SDK pins to a specific commit, so spec updates are controlled:
284
+ `spec/` is a submodule of [quickbase-spec](https://github.com/DrewBradfordXYZ/quickbase-spec), and CI fails when the generated types don't match it. The code in this README and `guides/` comes from `examples/docs.ts`, which `npm run typecheck` checks; change them together.
1211
285
 
1212
- ```bash
1213
- # Update to latest spec
1214
- cd spec
1215
- git pull origin main
1216
- cd ..
1217
- git add spec
1218
- git commit -m "Update quickbase-spec submodule"
1219
-
1220
- # Regenerate types
1221
- npm run spec:generate
1222
- ```
1223
-
1224
- ### Spec Tools
1225
-
1226
- ```bash
1227
- # Full spec processing pipeline
1228
- npm run spec:build
1229
-
1230
- # Individual steps
1231
- npm run spec:convert # Swagger 2.0 → OpenAPI 3.0
1232
- npm run spec:patch # Apply fixes from overrides
1233
- npm run spec:validate # Validate spec
1234
- npm run spec:split # Split by tag (for editing)
1235
- npm run spec:generate # Generate TypeScript types
1236
- ```
1237
-
1238
- ### Documentation
1239
-
1240
- API documentation is auto-generated from source code and hosted at:
1241
- **https://drewbradfordxyz.github.io/quickbase-js/**
1242
-
1243
- To generate docs locally:
1244
-
1245
- ```bash
1246
- npm run docs # Generate docs to ./docs
1247
- npm run docs:watch # Watch mode for development
1248
- ```
286
+ ## Related
1249
287
 
1250
- Documentation is built with [TypeDoc](https://typedoc.org/) and auto-deployed to GitHub Pages on every push to main.
288
+ - [quickbase-go](https://github.com/DrewBradfordXYZ/quickbase-go): the Go SDK, from the same spec
1251
289
 
1252
290
  ## License
1253
291