quickbase-js 2.2.2 → 2.4.0

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 (41) hide show
  1. package/README.md +107 -1129
  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/core/errors.d.ts +64 -0
  10. package/dist/core/errors.d.ts.map +1 -1
  11. package/dist/core/errors.js +64 -0
  12. package/dist/core/errors.js.map +1 -1
  13. package/dist/core/index.d.ts +1 -1
  14. package/dist/core/index.d.ts.map +1 -1
  15. package/dist/core/index.js +1 -1
  16. package/dist/core/index.js.map +1 -1
  17. package/dist/generated/client.d.ts +2 -0
  18. package/dist/generated/client.d.ts.map +1 -1
  19. package/dist/generated/client.js +272 -116
  20. package/dist/generated/client.js.map +1 -1
  21. package/dist/generated/types.d.ts +2880 -2122
  22. package/dist/generated/types.d.ts.map +1 -1
  23. package/dist/generated/types.js +0 -2
  24. package/dist/generated/types.js.map +1 -1
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/quickbase.esm.js +3 -3
  30. package/dist/quickbase.esm.js.map +3 -3
  31. package/dist/quickbase.min.js +3 -3
  32. package/dist/quickbase.min.js.map +3 -3
  33. package/dist/xml/errors.d.ts +21 -14
  34. package/dist/xml/errors.d.ts.map +1 -1
  35. package/dist/xml/errors.js +36 -22
  36. package/dist/xml/errors.js.map +1 -1
  37. package/dist/xml/index.d.ts +13 -1
  38. package/dist/xml/index.d.ts.map +1 -1
  39. package/dist/xml/index.js +28 -3
  40. package/dist/xml/index.js.map +1 -1
  41. package/package.json +4 -5
package/README.md CHANGED
@@ -1,1245 +1,223 @@
1
- # QuickBase JS SDK v2
1
+ # QuickBase JS SDK
2
2
 
3
- A TypeScript/JavaScript client for the QuickBase JSON RESTful API.
3
+ A TypeScript client for the [QuickBase JSON API](https://developer.quickbase.com/), for Node.js and the browser (including QuickBase code pages), with the legacy XML API for what JSON lacks.
4
4
 
5
- ## Features
5
+ - **Every operation** as a typed method, generated from a spec checked against live responses
6
+ - **Pagination** with `.all()` and `.paginate()`, and readable **schema aliases** instead of IDs
7
+ - **Temporary tokens in code pages**, fetched and refreshed for you from the user's session
8
+ - **Retries and throttling** for QuickBase's rate limits
9
+ - **Read-only mode** for tools that must never write
6
10
 
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
25
-
26
- ### npm / Node.js
11
+ **[API reference →](https://drewbradfordxyz.github.io/quickbase-js/)**
27
12
 
28
13
  ```bash
29
14
  npm install quickbase-js
30
15
  ```
31
16
 
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
- ```
68
-
69
- ## Quick Start
17
+ ## Quick start
70
18
 
71
19
  ```typescript
72
20
  import { createClient } from 'quickbase-js';
73
21
 
74
22
  const qb = createClient({
75
23
  realm: 'mycompany',
76
- auth: {
77
- type: 'user-token',
78
- userToken: 'your-user-token',
79
- },
24
+ auth: { type: 'user-token', userToken: process.env.QB_USER_TOKEN! },
80
25
  });
81
26
 
82
- // Use typed API methods
83
27
  const app = await qb.getApp({ appId: 'bpqe82s1' });
84
28
  console.log(app.name);
85
29
 
86
- // Query records
87
- const records = await qb.runQuery({
88
- from: 'bpqe82s1',
30
+ const result = await qb.runQuery({
31
+ from: 'bqtable1',
89
32
  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
33
+ where: "{7.EX.'Active'}",
34
+ }).all(); // every page
35
+ for (const record of result.data ?? []) {
36
+ console.log(record['6']?.value);
37
+ }
134
38
 
135
- ```typescript
136
- const qb = createClient({
137
- realm: 'mycompany',
138
- auth: {
139
- type: 'sso',
140
- samlToken: 'your-saml-token',
141
- },
39
+ await qb.upsert({
40
+ to: 'bqtable1',
41
+ data: [{ '6': 'New project', '7': 'Active' }], // plain values are wrapped for you
142
42
  });
143
43
  ```
144
44
 
145
- ### Ticket Auth (Username/Password)
45
+ ## In a code page
146
46
 
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.
47
+ In the browser, temporary-token auth uses the signed-in user's QuickBase session: no token in your code, and every call has that user's permissions.
148
48
 
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
- },
49
+ ```html
50
+ <script src="https://cdn.jsdelivr.net/npm/quickbase-js@2/dist/quickbase.min.js"></script>
51
+ <script>
52
+ const qb = QuickBase.createClient({
53
+ realm: window.location.hostname.split('.')[0],
54
+ auth: { type: 'temp-token' },
164
55
  });
165
- return qb;
166
- }
167
- ```
168
-
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)
174
-
175
- ## Pagination
176
-
177
- Paginated endpoints (`runQuery`, `runReport`, `getUsers`, etc.) return a `PaginatedRequest` that supports fluent pagination methods.
178
-
179
- ### Default Behavior (Single Page)
180
-
181
- ```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
186
- ```
187
-
188
- ### Fetch All Pages
189
-
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
56
+ qb.getApp({ appId: 'bpqe82s1' }).then((app) => console.log(app.name));
57
+ </script>
194
58
  ```
195
59
 
196
- ### Fetch with Record Limit
60
+ Or as a module: `import { createClient } from 'https://cdn.jsdelivr.net/npm/quickbase-js@2/dist/quickbase.esm.js'`.
197
61
 
198
- ```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
202
- ```
62
+ ## Typed methods for every operation
203
63
 
204
- ### Explicit Single Page
64
+ Each QuickBase operation is a method on the client, typed from [quickbase-spec](https://github.com/DrewBradfordXYZ/quickbase-spec), including properties QuickBase returns but doesn't document. Path and query parameters come first, then the body:
205
65
 
206
66
  ```typescript
207
- // Explicitly fetch single page (useful when autoPaginate is enabled globally)
208
- const single = await qb.runQuery({ from: 'tableId' }).noPaginate();
67
+ await qb.getFields({ tableId: 'bqtable1' });
68
+ await qb.runQuery({ from: 'bqtable1', select: [3, 6], sortBy: [{ fieldId: 3, order: 'ASC' }] });
69
+ await qb.deleteRecords({ from: 'bqtable1', where: '{3.EX.123}' });
70
+ await qb.updateApp({ appId: 'bpqe82s1' }, { name: 'Renamed' });
209
71
  ```
210
72
 
211
- ### Global Auto-Pagination
73
+ All of them, with QuickBase's descriptions: the [API reference](https://drewbradfordxyz.github.io/quickbase-js/interfaces/QuickbaseAPI.html).
212
74
 
213
- ```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();
225
- ```
226
-
227
- ## Date Conversion
228
-
229
- By default, ISO 8601 date strings in API responses are automatically converted to JavaScript `Date` objects:
230
-
231
- ```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
243
- });
244
- ```
75
+ ## Pagination
245
76
 
246
- To disable date conversion and keep dates as strings:
77
+ QuickBase returns a page of records at a time. Paginated methods (`runQuery`, `runReport`, `getUsers`, ...) return one page when awaited, or follow the pages:
247
78
 
248
79
  ```typescript
249
- const qb = createClient({
250
- realm: 'mycompany',
251
- auth: { type: 'user-token', userToken: 'token' },
252
- convertDates: false, // Keep dates as ISO strings
253
- });
80
+ const page = await qb.runQuery({ from: 'bqtable1' }); // one page
81
+ const all = await qb.runQuery({ from: 'bqtable1' }).all(); // every page
82
+ const first500 = await qb.runQuery({ from: 'bqtable1' }).paginate({ limit: 500 });
254
83
  ```
255
84
 
256
- ## Schema Aliases
85
+ `autoPaginate: true` makes a plain `await` fetch every page. See [guides/queries.md](guides/queries.md).
257
86
 
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.
87
+ ## Schema aliases
259
88
 
260
- ### Defining a Schema
89
+ Give the client names for your tables and fields, and use them instead of IDs. Requests are translated to IDs; responses come back keyed by name, with values unwrapped.
261
90
 
262
91
  ```typescript
263
92
  const qb = createClient({
264
93
  realm: 'mycompany',
265
94
  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
- },
95
+ schema: { tables: { projects: { id: 'bqw3ryzab', fields: { id: 3, name: 6, status: 7 } } } },
289
96
  });
290
- ```
291
97
 
292
- ### Using Aliases in Queries
293
-
294
- ```typescript
295
- // Use table and field aliases instead of IDs
296
98
  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
99
  from: 'projects',
357
- select: ['stauts'], // Typo!
100
+ select: ['name', 'status'],
101
+ where: "{'status'.EX.'Active'}",
358
102
  });
103
+ console.log(result.data?.[0]?.name);
359
104
  ```
360
105
 
361
- ### Generating Schema from QuickBase
362
-
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"
374
- ```
375
-
376
- The generator creates aliases from field labels using camelCase (e.g., "Due Date" → `dueDate`).
377
-
378
- ### Updating Schema with --merge
106
+ Requests accept aliases in TypeScript too. Results are typed by field ID, so read an aliased record with a cast. See [guides/schema-aliases.md](guides/schema-aliases.md), which also covers the schema generator (`npx quickbase-js schema`).
379
107
 
380
- When your QuickBase app changes (new tables, new fields), use `--merge` to update your schema while preserving any custom aliases you've set:
381
-
382
- ```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
385
- ```
386
-
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:
391
-
392
- ```
393
- Merge complete:
394
- Tables: 2 preserved, 1 added, 0 removed
395
- Fields: 15 preserved, 3 added, 0 removed
396
- ```
108
+ ## Authentication
397
109
 
398
- This lets you rename auto-generated aliases like `dateCreated` to `created` and keep them through updates.
110
+ | `auth.type` | Use it for |
111
+ |---|---|
112
+ | `'user-token'` | Servers and scripts |
113
+ | `'temp-token'` | Code pages and other browser code inside QuickBase |
114
+ | `'ticket'` | A user signing in with their QuickBase email and password |
115
+ | `'sso'` | A user signed in through your identity provider (SAML) |
399
116
 
400
- ### Using a Separate Schema File
117
+ Details: [guides/authentication.md](guides/authentication.md).
401
118
 
402
- You can define your schema in a separate file and import it:
119
+ ## Rate limits and retries
403
120
 
404
- **schema.ts**
405
- ```typescript
406
- import type { Schema } from 'quickbase-js';
121
+ 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:
407
122
 
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
- ```
422
-
423
- **client.ts**
424
123
  ```typescript
425
- import { createClient } from 'quickbase-js';
426
- import { schema } from './schema.js';
427
-
428
- const qb = createClient({
124
+ createClient({
429
125
  realm: 'mycompany',
430
126
  auth: { type: 'user-token', userToken: 'token' },
431
- schema,
432
- });
433
- ```
434
-
435
- Or use a JSON file:
436
-
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
- ```
448
-
449
- ```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
127
  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
- });
502
- ```
503
-
504
- ## XML API (Legacy Endpoints)
505
-
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
511
-
512
- ```typescript
513
- import { createClient, createXmlClient } from 'quickbase-js';
514
-
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
- const xml = createXmlClient(qb);
523
-
524
- // Get all roles in an app
525
- const roles = await xml.getRoleInfo(appId);
526
- 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
- },
128
+ proactiveThrottle: { enabled: true, requestsPer10Seconds: 100 },
129
+ retry: { maxAttempts: 5 },
764
130
  },
765
131
  });
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
132
  ```
784
133
 
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
- ```
134
+ See [guides/reliability.md](guides/reliability.md).
819
135
 
820
- **Responses transformed to keyed objects:**
136
+ ## Read-only mode
821
137
 
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 |
138
+ `readOnly: true` makes the client refuse every write before it is sent, for the JSON API and the XML client made from it. Reads that use POST, like `runQuery`, `runReport` and `audit`, still work. A blocked call throws `ReadOnlyError`.
830
139
 
831
- ## API Methods
832
-
833
- All QuickBase API endpoints are available as typed methods:
140
+ ## Errors
834
141
 
835
142
  ```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
- ```
143
+ import { RateLimitError, NotFoundError, ReadOnlyError, QuickbaseError } from 'quickbase-js';
884
144
 
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
145
  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
- }
969
- }
970
- ```
971
-
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
- ```
990
-
991
- ## Error Handling
992
-
993
- ```typescript
994
- import {
995
- QuickbaseError,
996
- RateLimitError,
997
- AuthenticationError,
998
- AuthorizationError,
999
- NotFoundError,
1000
- ValidationError,
1001
- ServerError,
1002
- ReadOnlyError,
1003
- } from 'quickbase-js';
1004
-
1005
- try {
1006
- await qb.getApp({ appId: 'invalid' });
146
+ await qb.getApp({ appId: 'bpqe82s1' });
1007
147
  } catch (error) {
1008
148
  if (error instanceof RateLimitError) {
1009
- console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
1010
- console.log(`Ray ID: ${error.rayId}`);
1011
- } else if (error instanceof AuthenticationError) {
1012
- console.log('Invalid or expired token');
1013
- } else if (error instanceof AuthorizationError) {
1014
- console.log('Permission denied');
149
+ console.log(`rate limited; retry after ${error.retryAfter}s`);
1015
150
  } else if (error instanceof NotFoundError) {
1016
- console.log('Resource not found');
1017
- } else if (error instanceof ValidationError) {
1018
- console.log('Bad request:', error.message);
1019
- } else if (error instanceof ServerError) {
1020
- console.log('Server error:', error.statusCode);
151
+ console.log('no such app');
152
+ } else if (error instanceof ReadOnlyError) {
153
+ console.log(`write blocked: ${error.method} ${error.path}`);
1021
154
  } else if (error instanceof QuickbaseError) {
1022
- console.log(`API error ${error.statusCode}: ${error.message}`);
155
+ console.log(`QuickBase error ${error.statusCode}: ${error.message}`);
1023
156
  }
1024
157
  }
1025
158
  ```
1026
159
 
1027
- ## Rate Limiting
1028
-
1029
- 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.
1030
-
1031
- ### How 429 Errors Are Handled
1032
-
1033
- When the SDK receives a 429 (Too Many Requests) response, it automatically:
1034
-
1035
- 1. **Extracts rate limit info** from response headers (`Retry-After`, `cf-ray`, `qb-api-ray`)
1036
- 2. **Calls the `onRateLimit` callback** if configured, allowing you to log or monitor
1037
- 3. **Waits before retrying** - uses the `Retry-After` header if present, otherwise exponential backoff with jitter
1038
- 4. **Retries the request** up to `maxAttempts` times (default: 3)
1039
- 5. **Throws a `RateLimitError`** if all retries are exhausted
160
+ The classes are `RateLimitError` (429), `AuthenticationError` (401), `AuthorizationError` (403), `NotFoundError` (404), `ValidationError` (400), `ServerError` (5xx) and `ReadOnlyError`, all extending `QuickbaseError`; the XML API adds `QuickbaseTicketError`, `QuickbaseAppTokenError` and `QuickbaseXmlError`.
1040
161
 
1041
- ```
1042
- Request fails with 429
1043
- ↓
1044
- Extract Retry-After header
1045
- ↓
1046
- Call onRateLimit callback (if set)
1047
- ↓
1048
- Wait (Retry-After or exponential backoff)
1049
- ↓
1050
- Retry request (up to maxAttempts)
1051
- ↓
1052
- Throw RateLimitError if exhausted
1053
- ```
162
+ ## Solutions and files
1054
163
 
1055
- ### Retry Configuration
164
+ Solutions are QBL (YAML) and file downloads are raw content, so these methods take and return text:
1056
165
 
1057
166
  ```typescript
1058
- const qb = createClient({
1059
- realm: 'mycompany',
1060
- auth: { type: 'user-token', userToken: 'token' },
1061
- rateLimit: {
1062
- retry: {
1063
- maxAttempts: 5, // Default: 3
1064
- initialDelayMs: 1000, // Default: 1000ms
1065
- maxDelayMs: 30000, // Default: 30000ms
1066
- multiplier: 2, // Default: 2
1067
- },
1068
- },
1069
- });
167
+ const qbl = await qb.exportSolution({ solutionId: 'solution-id' });
168
+ await qb.updateSolution({ solutionId: 'solution-id' }, qbl);
169
+ const file = await qb.downloadFile({ tableId: 'bqtable1', recordId: 12, fieldId: 8, versionNumber: 0 }); // base64
1070
170
  ```
1071
171
 
1072
- The backoff formula with jitter: `delay = initialDelayMs * (multiplier ^ attempt) ± 10%`
1073
-
1074
- ### Proactive Throttling
172
+ ## Dates
1075
173
 
1076
- Prevent 429 errors entirely by throttling requests client-side using a sliding window algorithm:
174
+ ISO date strings in responses become `Date` objects. The generated types say `string`, because QuickBase's spec marks few of its date fields as dates; set `convertDates: false` if you want values to match the types exactly.
1077
175
 
1078
- ```typescript
1079
- const qb = createClient({
1080
- realm: 'mycompany',
1081
- auth: { type: 'user-token', userToken: 'token' },
1082
- rateLimit: {
1083
- proactiveThrottle: {
1084
- enabled: true,
1085
- requestsPer10Seconds: 100,
1086
- },
1087
- },
1088
- });
1089
- ```
176
+ ## Raw requests
1090
177
 
1091
- This tracks request timestamps and blocks new requests when the limit would be exceeded, waiting until the oldest request exits the 10-second window.
178
+ `qb.request<T>({ method, path, body, query })` calls any endpoint with the client's authentication, retries and throttling.
1092
179
 
1093
- ### Rate Limit Callback
180
+ ## The legacy XML API
1094
181
 
1095
- Get notified when rate limited (called before retry):
182
+ `createXmlClient(qb)` reaches what the JSON API doesn't: roles, groups, code pages, app variables, webhooks, CSV imports. It shares the client's authentication, retries, read-only mode and schema.
1096
183
 
1097
184
  ```typescript
1098
- const qb = createClient({
1099
- realm: 'mycompany',
1100
- auth: { type: 'user-token', userToken: 'token' },
1101
- rateLimit: {
1102
- onRateLimit: (info) => {
1103
- console.log(`Rate limited on ${info.requestUrl}`);
1104
- console.log(`Retry after: ${info.retryAfter} seconds`);
1105
- console.log(`Ray ID: ${info.qbApiRay}`);
1106
- console.log(`Attempt: ${info.attempt}`);
1107
- },
1108
- },
1109
- });
1110
- ```
1111
-
1112
- ### Handling RateLimitError
1113
-
1114
- If retries are exhausted, a `RateLimitError` is thrown:
1115
-
1116
- ```typescript
1117
- import { RateLimitError } from 'quickbase-js';
185
+ import { createXmlClient } from 'quickbase-js';
1118
186
 
1119
- try {
1120
- await qb.getApp({ appId: 'bpqe82s1' });
1121
- } catch (error) {
1122
- if (error instanceof RateLimitError) {
1123
- console.log(`Rate limited after ${error.rateLimitInfo.attempt} attempts`);
1124
- console.log(`Retry after: ${error.retryAfter} seconds`);
1125
- }
187
+ const xml = createXmlClient(qb);
188
+ const roles = await xml.getRoleInfo(appId);
189
+ for (const role of roles.roles) {
190
+ console.log(role.name, role.access.description);
1126
191
  }
1127
192
  ```
1128
193
 
1129
- ## Migration from v1
1130
-
1131
- ### Authentication
1132
-
1133
- ```typescript
1134
- // v1 - user token
1135
- const qb = quickbase({ realm: 'company', userToken: 'token' });
1136
-
1137
- // v2 - user token (explicit type)
1138
- const qb = createClient({
1139
- realm: 'company',
1140
- auth: { type: 'user-token', userToken: 'token' },
1141
- });
1142
-
1143
- // v1 - temp tokens in Code Pages
1144
- const qb = quickbase({ realm: 'company', useTempTokens: true });
1145
-
1146
- // v2 - temp tokens in Code Pages
1147
- const qb = createClient({
1148
- realm: 'company',
1149
- auth: { type: 'temp-token' },
1150
- });
1151
- ```
1152
-
1153
- ### Pagination
194
+ Every method: [guides/xml-api.md](guides/xml-api.md).
1154
195
 
1155
- ```typescript
1156
- // v1 - callback-based
1157
- await qb.withPaginationDisabled(async () => qb.runQuery({ body: { from: tableId } }));
1158
- await qb.withPaginationLimit(100, async () => qb.runQuery({ body: { from: tableId } }));
1159
-
1160
- // v2 - fluent API
1161
- await qb.runQuery({ from: tableId }).noPaginate();
1162
- await qb.runQuery({ from: tableId }).paginate({ limit: 100 });
1163
- ```
1164
-
1165
- ### Request Body
196
+ ## Guides
1166
197
 
1167
- ```typescript
1168
- // v1 - body wrapper
1169
- await qb.runQuery({ body: { from: tableId, select: [3, 6] } });
1170
-
1171
- // v2 - direct parameters
1172
- await qb.runQuery({ from: tableId, select: [3, 6] });
1173
- ```
198
+ - [Queries, pagination and records](guides/queries.md)
199
+ - [Schema aliases](guides/schema-aliases.md)
200
+ - [Authentication](guides/authentication.md)
201
+ - [Rate limits and retries](guides/reliability.md)
202
+ - [The XML API](guides/xml-api.md)
203
+ - [Migrating from v1](guides/migration.md)
204
+ - [API reference](https://drewbradfordxyz.github.io/quickbase-js/) and [CHANGELOG](CHANGELOG.md)
1174
205
 
1175
206
  ## Development
1176
207
 
1177
208
  ```bash
1178
- # Clone with submodules (includes OpenAPI spec)
1179
209
  git clone --recurse-submodules https://github.com/DrewBradfordXYZ/quickbase-js.git
1180
-
1181
- # Or if already cloned, initialize submodules
1182
- git submodule update --init
1183
-
1184
- # Install dependencies
1185
210
  npm install
1186
-
1187
- # Run tests
1188
- npm test
1189
-
1190
- # Build
1191
- npm run build
1192
-
1193
- # Type check
1194
- npm run typecheck
1195
-
1196
- # Generate types from OpenAPI spec
1197
- npm run spec:generate
1198
- ```
1199
-
1200
- ### Updating the OpenAPI Spec
1201
-
1202
- 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:
1203
-
1204
- ```bash
1205
- # Update to latest spec
1206
- cd spec
1207
- git pull origin main
1208
- cd ..
1209
- git add spec
1210
- git commit -m "Update quickbase-spec submodule"
1211
-
1212
- # Regenerate types
1213
- npm run spec:generate
1214
- ```
1215
-
1216
- ### Spec Tools
1217
-
1218
- ```bash
1219
- # Full spec processing pipeline
1220
- npm run spec:build
1221
-
1222
- # Individual steps
1223
- npm run spec:convert # Swagger 2.0 → OpenAPI 3.0
1224
- npm run spec:patch # Apply fixes from overrides
1225
- npm run spec:validate # Validate spec
1226
- npm run spec:split # Split by tag (for editing)
1227
- npm run spec:generate # Generate TypeScript types
211
+ npm test # unit tests
212
+ npm run typecheck # the library, and the docs' examples
213
+ npm run spec:build # regenerate the types from the pinned spec in spec/
1228
214
  ```
1229
215
 
1230
- ### Documentation
1231
-
1232
- API documentation is auto-generated from source code and hosted at:
1233
- **https://drewbradfordxyz.github.io/quickbase-js/**
216
+ `spec/` is a submodule of [quickbase-spec](https://github.com/DrewBradfordXYZ/quickbase-spec). CI requires the generated types to match it. The code in this README and `guides/` comes from `examples/docs.ts`, which `npm run typecheck` checks (aliased examples there carry the casts TypeScript needs).
1234
217
 
1235
- To generate docs locally:
1236
-
1237
- ```bash
1238
- npm run docs # Generate docs to ./docs
1239
- npm run docs:watch # Watch mode for development
1240
- ```
218
+ ## Related
1241
219
 
1242
- Documentation is built with [TypeDoc](https://typedoc.org/) and auto-deployed to GitHub Pages on every push to main.
220
+ - [quickbase-go](https://github.com/DrewBradfordXYZ/quickbase-go): the Go SDK, from the same spec
1243
221
 
1244
222
  ## License
1245
223