quickbase-js 2.1.0 → 2.2.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 (112) hide show
  1. package/README.md +674 -90
  2. package/bin/quickbase-js.js +54 -0
  3. package/dist/auth/index.d.ts.map +1 -1
  4. package/dist/auth/index.js +5 -1
  5. package/dist/auth/index.js.map +1 -1
  6. package/dist/auth/ticket.d.ts +13 -0
  7. package/dist/auth/ticket.d.ts.map +1 -1
  8. package/dist/auth/ticket.js +103 -3
  9. package/dist/auth/ticket.js.map +1 -1
  10. package/dist/cli/schema.d.ts +23 -0
  11. package/dist/cli/schema.d.ts.map +1 -0
  12. package/dist/cli/schema.js +425 -0
  13. package/dist/cli/schema.js.map +1 -0
  14. package/dist/client/request.d.ts +2 -1
  15. package/dist/client/request.d.ts.map +1 -1
  16. package/dist/client/request.js +165 -18
  17. package/dist/client/request.js.map +1 -1
  18. package/dist/core/config.d.ts.map +1 -1
  19. package/dist/core/config.js +3 -0
  20. package/dist/core/config.js.map +1 -1
  21. package/dist/core/errors.d.ts +17 -0
  22. package/dist/core/errors.d.ts.map +1 -1
  23. package/dist/core/errors.js +32 -0
  24. package/dist/core/errors.js.map +1 -1
  25. package/dist/core/index.d.ts +2 -2
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/index.js +1 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/schema.d.ts +37 -0
  30. package/dist/core/schema.d.ts.map +1 -0
  31. package/dist/core/schema.js +168 -0
  32. package/dist/core/schema.js.map +1 -0
  33. package/dist/core/transform.d.ts +36 -0
  34. package/dist/core/transform.d.ts.map +1 -0
  35. package/dist/core/transform.js +205 -0
  36. package/dist/core/transform.js.map +1 -0
  37. package/dist/core/types.d.ts +87 -0
  38. package/dist/core/types.d.ts.map +1 -1
  39. package/dist/core/types.js.map +1 -1
  40. package/dist/index.d.ts +11 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/quickbase.esm.js +3 -3
  45. package/dist/quickbase.esm.js.map +4 -4
  46. package/dist/quickbase.min.js +3 -3
  47. package/dist/quickbase.min.js.map +4 -4
  48. package/dist/xml/codepages.d.ts +60 -0
  49. package/dist/xml/codepages.d.ts.map +1 -0
  50. package/dist/xml/codepages.js +88 -0
  51. package/dist/xml/codepages.js.map +1 -0
  52. package/dist/xml/dbvars.d.ts +39 -0
  53. package/dist/xml/dbvars.d.ts.map +1 -0
  54. package/dist/xml/dbvars.js +54 -0
  55. package/dist/xml/dbvars.js.map +1 -0
  56. package/dist/xml/errors.d.ts +64 -0
  57. package/dist/xml/errors.d.ts.map +1 -0
  58. package/dist/xml/errors.js +102 -0
  59. package/dist/xml/errors.js.map +1 -0
  60. package/dist/xml/fields.d.ts +45 -0
  61. package/dist/xml/fields.d.ts.map +1 -0
  62. package/dist/xml/fields.js +86 -0
  63. package/dist/xml/fields.js.map +1 -0
  64. package/dist/xml/groups.d.ts +196 -0
  65. package/dist/xml/groups.d.ts.map +1 -0
  66. package/dist/xml/groups.js +424 -0
  67. package/dist/xml/groups.js.map +1 -0
  68. package/dist/xml/html.d.ts +61 -0
  69. package/dist/xml/html.d.ts.map +1 -0
  70. package/dist/xml/html.js +110 -0
  71. package/dist/xml/html.js.map +1 -0
  72. package/dist/xml/index.d.ts +453 -0
  73. package/dist/xml/index.d.ts.map +1 -0
  74. package/dist/xml/index.js +741 -0
  75. package/dist/xml/index.js.map +1 -0
  76. package/dist/xml/metadata.d.ts +49 -0
  77. package/dist/xml/metadata.d.ts.map +1 -0
  78. package/dist/xml/metadata.js +84 -0
  79. package/dist/xml/metadata.js.map +1 -0
  80. package/dist/xml/records.d.ts +106 -0
  81. package/dist/xml/records.d.ts.map +1 -0
  82. package/dist/xml/records.js +220 -0
  83. package/dist/xml/records.js.map +1 -0
  84. package/dist/xml/request.d.ts +115 -0
  85. package/dist/xml/request.d.ts.map +1 -0
  86. package/dist/xml/request.js +167 -0
  87. package/dist/xml/request.js.map +1 -0
  88. package/dist/xml/roles.d.ts +117 -0
  89. package/dist/xml/roles.d.ts.map +1 -0
  90. package/dist/xml/roles.js +241 -0
  91. package/dist/xml/roles.js.map +1 -0
  92. package/dist/xml/schema.d.ts +94 -0
  93. package/dist/xml/schema.d.ts.map +1 -0
  94. package/dist/xml/schema.js +354 -0
  95. package/dist/xml/schema.js.map +1 -0
  96. package/dist/xml/transform.d.ts +154 -0
  97. package/dist/xml/transform.d.ts.map +1 -0
  98. package/dist/xml/transform.js +182 -0
  99. package/dist/xml/transform.js.map +1 -0
  100. package/dist/xml/types.d.ts +531 -0
  101. package/dist/xml/types.d.ts.map +1 -0
  102. package/dist/xml/types.js +22 -0
  103. package/dist/xml/types.js.map +1 -0
  104. package/dist/xml/users.d.ts +92 -0
  105. package/dist/xml/users.d.ts.map +1 -0
  106. package/dist/xml/users.js +166 -0
  107. package/dist/xml/users.js.map +1 -0
  108. package/dist/xml/webhooks.d.ts +98 -0
  109. package/dist/xml/webhooks.d.ts.map +1 -0
  110. package/dist/xml/webhooks.js +202 -0
  111. package/dist/xml/webhooks.js.map +1 -0
  112. package/package.json +6 -2
package/README.md CHANGED
@@ -5,12 +5,15 @@ A TypeScript/JavaScript client for the QuickBase JSON RESTful API.
5
5
  ## Features
6
6
 
7
7
  - **Typed API Methods** - Full TypeScript support with auto-generated types from OpenAPI spec
8
- - **Multiple Auth Methods** - User token, temporary token, and SSO authentication
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
9
10
  - **Fluent Pagination** - Chain `.all()`, `.paginate()`, or `.noPaginate()` on paginated endpoints
10
11
  - **Date Conversion** - Automatic conversion of ISO date strings to JavaScript Date objects
11
12
  - **Rate Limit Handling** - Automatic retry with exponential backoff on 429 errors
12
13
  - **Proactive Throttling** - Optional client-side request throttling
13
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
14
17
 
15
18
  ## API Reference
16
19
 
@@ -36,12 +39,12 @@ For QuickBase Code Pages, use temp-token authentication which automatically leve
36
39
  // Realm can be auto-detected from the current URL
37
40
  const realm = window.location.hostname.split('.')[0];
38
41
 
39
- const client = QuickBase.createClient({
42
+ const qb = QuickBase.createClient({
40
43
  realm: realm,
41
44
  auth: { type: 'temp-token' },
42
45
  });
43
46
 
44
- client.getApp({ appId: 'bpqe82s1' }).then(app => {
47
+ qb.getApp({ appId: 'bpqe82s1' }).then(app => {
45
48
  console.log(app.name);
46
49
  });
47
50
  </script>
@@ -53,12 +56,12 @@ Or with ES modules:
53
56
  <script type="module">
54
57
  import { createClient } from 'https://cdn.jsdelivr.net/npm/quickbase-js@2/dist/quickbase.esm.js';
55
58
 
56
- const client = createClient({
59
+ const qb = createClient({
57
60
  realm: 'mycompany',
58
61
  auth: { type: 'temp-token' },
59
62
  });
60
63
 
61
- const app = await client.getApp({ appId: 'bpqe82s1' });
64
+ const app = await qb.getApp({ appId: 'bpqe82s1' });
62
65
  console.log(app.name);
63
66
  </script>
64
67
  ```
@@ -68,7 +71,7 @@ Or with ES modules:
68
71
  ```typescript
69
72
  import { createClient } from 'quickbase-js';
70
73
 
71
- const client = createClient({
74
+ const qb = createClient({
72
75
  realm: 'mycompany',
73
76
  auth: {
74
77
  type: 'user-token',
@@ -77,11 +80,11 @@ const client = createClient({
77
80
  });
78
81
 
79
82
  // Use typed API methods
80
- const app = await client.getApp({ appId: 'bpqe82s1' });
83
+ const app = await qb.getApp({ appId: 'bpqe82s1' });
81
84
  console.log(app.name);
82
85
 
83
86
  // Query records
84
- const records = await client.runQuery({
87
+ const records = await qb.runQuery({
85
88
  from: 'bpqe82s1',
86
89
  select: [3, 6, 7],
87
90
  where: "{6.EX.'Active'}",
@@ -94,7 +97,7 @@ console.log(records.data);
94
97
  ### User Token (Recommended for Server-Side)
95
98
 
96
99
  ```typescript
97
- const client = createClient({
100
+ const qb = createClient({
98
101
  realm: 'mycompany',
99
102
  auth: {
100
103
  type: 'user-token',
@@ -105,26 +108,16 @@ const client = createClient({
105
108
 
106
109
  ### Temporary Token (Recommended for Browser/Code Pages)
107
110
 
108
- Temporary tokens are automatically fetched and cached per database. They refresh on 401 errors.
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.
109
112
 
110
113
  ```typescript
111
- // In QuickBase Code Pages - no user token needed (uses browser session)
112
- const client = createClient({
114
+ // In QuickBase Code Pages - uses browser session for authentication
115
+ const qb = createClient({
113
116
  realm: 'mycompany',
114
117
  auth: {
115
118
  type: 'temp-token',
116
119
  },
117
120
  });
118
-
119
- // Outside Code Pages - user token required to fetch temp tokens
120
- const client = createClient({
121
- realm: 'mycompany',
122
- auth: {
123
- type: 'temp-token',
124
- userToken: 'your-user-token', // Required outside Code Pages
125
- appToken: 'optional-app-token', // If app requires it
126
- },
127
- });
128
121
  ```
129
122
 
130
123
  **Auto dbid extraction:** The SDK automatically extracts the database ID for temp token authentication from your request parameters:
@@ -140,7 +133,7 @@ This means you don't need to manually specify the dbid - just make API calls and
140
133
  ### SSO Token
141
134
 
142
135
  ```typescript
143
- const client = createClient({
136
+ const qb = createClient({
144
137
  realm: 'mycompany',
145
138
  auth: {
146
139
  type: 'sso',
@@ -151,43 +144,33 @@ const client = createClient({
151
144
 
152
145
  ### Ticket Auth (Username/Password)
153
146
 
154
- 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.
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.
155
148
 
156
149
  ```typescript
157
- const client = createClient({
158
- realm: 'mycompany',
159
- auth: {
160
- type: 'ticket',
161
- username: 'user@example.com',
162
- password: 'password',
163
- },
164
- });
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
+ },
164
+ });
165
+ return qb;
166
+ }
165
167
  ```
166
168
 
167
169
  **Key behaviors:**
168
170
  - Authentication happens lazily on the first API call
169
- - Password is discarded from memory after authentication
170
- - Tickets are valid for 12 hours by default (configurable up to ~6 months)
171
- - When the ticket expires, an error is thrown — create a new client with fresh credentials
172
-
173
- **With custom ticket validity:**
174
-
175
- ```typescript
176
- const client = createClient({
177
- realm: 'mycompany',
178
- auth: {
179
- type: 'ticket',
180
- username: 'user@example.com',
181
- password: 'password',
182
- hours: 24 * 7, // 1 week
183
- },
184
- });
185
- ```
186
-
187
- **When to use ticket auth:**
188
- - Third-party services where users shouldn't share user tokens
189
- - Proper audit trails with correct `createdBy`/`modifiedBy` attribution
190
- - Session-based authentication flows
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)
191
174
 
192
175
  ## Pagination
193
176
 
@@ -197,7 +180,7 @@ Paginated endpoints (`runQuery`, `runReport`, `getUsers`, etc.) return a `Pagina
197
180
 
198
181
  ```typescript
199
182
  // Returns first page only
200
- const page = await client.runQuery({ from: 'tableId' });
183
+ const page = await qb.runQuery({ from: 'tableId' });
201
184
  console.log(page.data.length); // e.g., 100 records
202
185
  console.log(page.metadata.totalRecords); // e.g., 5000 total
203
186
  ```
@@ -206,7 +189,7 @@ console.log(page.metadata.totalRecords); // e.g., 5000 total
206
189
 
207
190
  ```typescript
208
191
  // Automatically fetches all pages and combines results
209
- const all = await client.runQuery({ from: 'tableId' }).all();
192
+ const all = await qb.runQuery({ from: 'tableId' }).all();
210
193
  console.log(all.data.length); // 5000 records
211
194
  ```
212
195
 
@@ -214,7 +197,7 @@ console.log(all.data.length); // 5000 records
214
197
 
215
198
  ```typescript
216
199
  // Fetch up to 500 records (may span multiple pages)
217
- const limited = await client.runQuery({ from: 'tableId' }).paginate({ limit: 500 });
200
+ const limited = await qb.runQuery({ from: 'tableId' }).paginate({ limit: 500 });
218
201
  console.log(limited.data.length); // 500 records
219
202
  ```
220
203
 
@@ -222,23 +205,23 @@ console.log(limited.data.length); // 500 records
222
205
 
223
206
  ```typescript
224
207
  // Explicitly fetch single page (useful when autoPaginate is enabled globally)
225
- const single = await client.runQuery({ from: 'tableId' }).noPaginate();
208
+ const single = await qb.runQuery({ from: 'tableId' }).noPaginate();
226
209
  ```
227
210
 
228
211
  ### Global Auto-Pagination
229
212
 
230
213
  ```typescript
231
- const client = createClient({
214
+ const qb = createClient({
232
215
  realm: 'mycompany',
233
216
  auth: { type: 'user-token', userToken: 'token' },
234
217
  autoPaginate: true, // Default await now fetches all pages
235
218
  });
236
219
 
237
220
  // Now direct await fetches all pages
238
- const all = await client.runQuery({ from: 'tableId' });
221
+ const all = await qb.runQuery({ from: 'tableId' });
239
222
 
240
223
  // Use .noPaginate() to get single page
241
- const page = await client.runQuery({ from: 'tableId' }).noPaginate();
224
+ const page = await qb.runQuery({ from: 'tableId' }).noPaginate();
242
225
  ```
243
226
 
244
227
  ## Date Conversion
@@ -246,14 +229,14 @@ const page = await client.runQuery({ from: 'tableId' }).noPaginate();
246
229
  By default, ISO 8601 date strings in API responses are automatically converted to JavaScript `Date` objects:
247
230
 
248
231
  ```typescript
249
- const app = await client.getApp({ appId: 'bpqe82s1' });
232
+ const app = await qb.getApp({ appId: 'bpqe82s1' });
250
233
 
251
234
  // Date fields are now Date objects
252
235
  console.log(app.created instanceof Date); // true
253
236
  console.log(app.created.toISOString()); // "2024-01-15T10:30:00.000Z"
254
237
 
255
238
  // Works with nested objects and arrays
256
- const records = await client.runQuery({ from: 'tableId' });
239
+ const records = await qb.runQuery({ from: 'tableId' });
257
240
  records.data.forEach(record => {
258
241
  // Date field values are converted
259
242
  console.log(record['7'].value instanceof Date); // true for date/timestamp fields
@@ -263,17 +246,221 @@ records.data.forEach(record => {
263
246
  To disable date conversion and keep dates as strings:
264
247
 
265
248
  ```typescript
266
- const client = createClient({
249
+ const qb = createClient({
267
250
  realm: 'mycompany',
268
251
  auth: { type: 'user-token', userToken: 'token' },
269
252
  convertDates: false, // Keep dates as ISO strings
270
253
  });
271
254
  ```
272
255
 
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.
259
+
260
+ ### Defining a Schema
261
+
262
+ ```typescript
263
+ const qb = createClient({
264
+ realm: 'mycompany',
265
+ 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
+ },
289
+ });
290
+ ```
291
+
292
+ ### Using Aliases in Queries
293
+
294
+ ```typescript
295
+ // Use table and field aliases instead of IDs
296
+ 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
+ from: 'projects',
357
+ select: ['stauts'], // Typo!
358
+ });
359
+ ```
360
+
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
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:
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
+ ```
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
+ ```
422
+
423
+ **client.ts**
424
+ ```typescript
425
+ import { createClient } from 'quickbase-js';
426
+ import { schema } from './schema.js';
427
+
428
+ const qb = createClient({
429
+ realm: 'mycompany',
430
+ 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
+
273
460
  ## Configuration Options
274
461
 
275
462
  ```typescript
276
- const client = createClient({
463
+ const qb = createClient({
277
464
  // Required
278
465
  realm: 'mycompany',
279
466
  auth: { /* see authentication section */ },
@@ -284,6 +471,8 @@ const client = createClient({
284
471
  baseUrl: 'https://api.quickbase.com/v1', // API base URL
285
472
  autoPaginate: false, // Auto-paginate on direct await
286
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
287
476
 
288
477
  // Rate limiting
289
478
  rateLimit: {
@@ -312,26 +501,327 @@ const client = createClient({
312
501
  });
313
502
  ```
314
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
+ ### Available Methods
544
+
545
+ **App Discovery:**
546
+
547
+ | Method | XML Action | Description |
548
+ |--------|------------|-------------|
549
+ | `grantedDBs(opts?)` | API_GrantedDBs | List all apps/tables user can access |
550
+ | `findDBByName(name, parentsOnly?)` | API_FindDBByName | Find an app by name |
551
+ | `getDBInfo(dbid)` | API_GetDBInfo | Get app/table metadata (record count, manager, timestamps) |
552
+ | `getNumRecords(tableId)` | API_GetNumRecords | Get total record count for a table |
553
+
554
+ **Role Management:**
555
+
556
+ | Method | XML Action | Description |
557
+ |--------|------------|-------------|
558
+ | `getRoleInfo(appId)` | API_GetRoleInfo | Get all roles defined in an application |
559
+ | `userRoles(appId)` | API_UserRoles | Get all users and their role assignments |
560
+ | `getUserRole(appId, userId, includeGroups?)` | API_GetUserRole | Get roles for a specific user |
561
+ | `addUserToRole(appId, userId, roleId)` | API_AddUserToRole | Assign a user to a role |
562
+ | `removeUserFromRole(appId, userId, roleId)` | API_RemoveUserFromRole | Remove a user from a role |
563
+ | `changeUserRole(appId, userId, currentRole, newRole)` | API_ChangeUserRole | Change a user's role |
564
+
565
+ **User Management:**
566
+
567
+ | Method | XML Action | Description |
568
+ |--------|------------|-------------|
569
+ | `getUserInfo(email)` | API_GetUserInfo | Get user info by email address |
570
+ | `provisionUser(appId, email, opts?)` | API_ProvisionUser | Create a new unregistered user |
571
+ | `sendInvitation(appId, userId, userText?)` | API_SendInvitation | Send invitation email to a user |
572
+ | `changeManager(appId, newManagerEmail)` | API_ChangeManager | Change the app manager |
573
+ | `changeRecordOwner(tableId, recordId, newOwner)` | API_ChangeRecordOwner | Change record owner |
574
+
575
+ **Group Management:**
576
+
577
+ | Method | XML Action | Description |
578
+ |--------|------------|-------------|
579
+ | `createGroup(name, opts?)` | API_CreateGroup | Create a new group |
580
+ | `deleteGroup(groupId)` | API_DeleteGroup | Delete a group |
581
+ | `copyGroup(groupId, name, opts?)` | API_CopyGroup | Copy a group |
582
+ | `changeGroupInfo(groupId, opts)` | API_ChangeGroupInfo | Update group name/description |
583
+ | `getUsersInGroup(groupId, includeManagers?)` | API_GetUsersInGroup | Get users and managers in a group |
584
+ | `addUserToGroup(groupId, userId, allowAdmin?)` | API_AddUserToGroup | Add a user to a group |
585
+ | `removeUserFromGroup(groupId, userId)` | API_RemoveUserFromGroup | Remove a user from a group |
586
+ | `getGroupRole(appId, groupId)` | API_GetGroupRole | Get roles assigned to a group |
587
+ | `addGroupToRole(appId, groupId, roleId)` | API_AddGroupToRole | Assign a group to a role |
588
+ | `removeGroupFromRole(appId, groupId, roleId, allRoles?)` | API_RemoveGroupFromRole | Remove a group from a role |
589
+ | `grantedGroups(userId?, adminOnly?)` | API_GrantedGroups | Get groups a user belongs to |
590
+ | `grantedDBsForGroup(groupId)` | API_GrantedDBsForGroup | Get apps a group can access |
591
+
592
+ **App Metadata:**
593
+
594
+ | Method | XML Action | Description |
595
+ |--------|------------|-------------|
596
+ | `getAppDTMInfo(appId)` | API_GetAppDTMInfo | Get modification timestamps |
597
+ | `getAncestorInfo(appId)` | API_GetAncestorInfo | Get app copy/template lineage info |
598
+
599
+ **Application Variables:**
600
+
601
+ | Method | XML Action | Description |
602
+ |--------|------------|-------------|
603
+ | `getDBVar(appId, varName)` | API_GetDBVar | Get an application variable value |
604
+ | `setDBVar(appId, varName, value)` | API_SetDBVar | Set an application variable value |
605
+
606
+ **Code Pages:**
607
+
608
+ | Method | XML Action | Description |
609
+ |--------|------------|-------------|
610
+ | `getDBPage(appId, pageIdOrName)` | API_GetDBPage | Get stored code page content |
611
+ | `addReplaceDBPage(appId, opts)` | API_AddReplaceDBPage | Create or update a code page |
612
+
613
+ **Field Management:**
614
+
615
+ | Method | XML Action | Description |
616
+ |--------|------------|-------------|
617
+ | `fieldAddChoices(tableId, fieldId, choices)` | API_FieldAddChoices | Add choices to a multiple-choice field |
618
+ | `fieldRemoveChoices(tableId, fieldId, choices)` | API_FieldRemoveChoices | Remove choices from a field |
619
+ | `setKeyField(tableId, fieldId)` | API_SetKeyField | Set the key field for a table |
620
+
621
+ **Schema Information:**
622
+
623
+ | Method | XML Action | Description |
624
+ |--------|------------|-------------|
625
+ | `getSchema(dbid)` | API_GetSchema | Get comprehensive app/table metadata |
626
+
627
+ **Record Information:**
628
+
629
+ | Method | XML Action | Description |
630
+ |--------|------------|-------------|
631
+ | `doQueryCount(tableId, query?)` | API_DoQueryCount | Get count of matching records (no data fetch) |
632
+ | `getRecordInfo(tableId, recordId)` | API_GetRecordInfo | Get record with field metadata |
633
+ | `getRecordInfoByKey(tableId, keyValue)` | API_GetRecordInfo | Get record by key field value |
634
+
635
+ **Record Operations:**
636
+
637
+ | Method | XML Action | Description |
638
+ |--------|------------|-------------|
639
+ | `copyMasterDetail(tableId, opts)` | API_CopyMasterDetail | Copy a master record with its detail records |
640
+ | `importFromCSV(tableId, opts)` | API_ImportFromCSV | Bulk import/update records from CSV data |
641
+ | `runImport(tableId, importId)` | API_RunImport | Execute a saved import definition |
642
+
643
+ **Webhooks:**
644
+
645
+ | Method | XML Action | Description |
646
+ |--------|------------|-------------|
647
+ | `webhooksCreate(tableId, opts)` | API_Webhooks_Create | Create a webhook |
648
+ | `webhooksEdit(tableId, webhookId, opts)` | API_Webhooks_Edit | Edit a webhook |
649
+ | `webhooksDelete(tableId, webhookId)` | API_Webhooks_Delete | Delete a webhook |
650
+ | `webhooksActivate(tableId, webhookId)` | API_Webhooks_Activate | Activate a webhook |
651
+ | `webhooksDeactivate(tableId, webhookId)` | API_Webhooks_Deactivate | Deactivate a webhook |
652
+ | `webhooksCopy(tableId, webhookId, name?)` | API_Webhooks_Copy | Copy a webhook |
653
+
654
+ **HTML Generation:**
655
+
656
+ | Method | XML Action | Description |
657
+ |--------|------------|-------------|
658
+ | `genAddRecordForm(tableId, fields?)` | API_GenAddRecordForm | Generate HTML form for adding a record |
659
+ | `genResultsTable(tableId, opts?)` | API_GenResultsTable | Generate HTML/JS/CSV table of query results |
660
+ | `getRecordAsHTML(tableId, opts)` | API_GetRecordAsHTML | Get a record rendered as HTML |
661
+
662
+ **Authentication:**
663
+
664
+ | Method | XML Action | Description |
665
+ |--------|------------|-------------|
666
+ | `signOut()` | API_SignOut | Clear ticket cookie (browser-focused) |
667
+
668
+ ### Error Handling
669
+
670
+ XML API errors are returned as `XmlError`:
671
+
672
+ ```typescript
673
+ import { XmlError, isUnauthorized, isNotFound, isInvalidTicket } from 'quickbase-js';
674
+
675
+ try {
676
+ const roles = await xml.getRoleInfo(appId);
677
+ } catch (error) {
678
+ if (error instanceof XmlError) {
679
+ console.log(`XML API error ${error.code}: ${error.text}`);
680
+
681
+ // Use helper functions for common error types
682
+ if (isUnauthorized(error)) {
683
+ console.log('Not authorized');
684
+ } else if (isNotFound(error)) {
685
+ console.log('Resource not found');
686
+ } else if (isInvalidTicket(error)) {
687
+ console.log('Invalid or expired ticket');
688
+ }
689
+ }
690
+ }
691
+ ```
692
+
693
+ ### Read-Only Mode
694
+
695
+ The XML client inherits read-only mode from the main client (see [Read-Only Mode](#read-only-mode-1)):
696
+
697
+ ```typescript
698
+ const qb = createClient({
699
+ realm: 'mycompany',
700
+ auth: { type: 'user-token', userToken: 'token' },
701
+ readOnly: true,
702
+ });
703
+
704
+ const xml = createXmlClient(qb); // Inherits readOnly: true
705
+
706
+ // Read operations work
707
+ const roles = await xml.getRoleInfo(appId);
708
+
709
+ // Write operations throw ReadOnlyError
710
+ await xml.addUserToRole(appId, userId, roleId); // Throws!
711
+ ```
712
+
713
+ You can also override read-only mode for just the XML client:
714
+
715
+ ```typescript
716
+ const xml = createXmlClient(qb, { readOnly: true });
717
+ ```
718
+
719
+ ### Schema Aliases
720
+
721
+ The XML client inherits the schema from the main client and supports table and field aliases:
722
+
723
+ ```typescript
724
+ const qb = createClient({
725
+ realm: 'mycompany',
726
+ auth: { type: 'user-token', userToken: 'token' },
727
+ schema: {
728
+ tables: {
729
+ projects: {
730
+ id: 'bqprojects123',
731
+ fields: {
732
+ id: 3,
733
+ name: 6,
734
+ status: 7,
735
+ },
736
+ },
737
+ },
738
+ },
739
+ });
740
+
741
+ const xml = createXmlClient(qb);
742
+
743
+ // Use table aliases
744
+ const count = await xml.getNumRecords('projects'); // Resolves to 'bqprojects123'
745
+ const roles = await xml.getRoleInfo('projects');
746
+
747
+ // Use field aliases
748
+ await xml.fieldAddChoices('projects', 'status', ['Active', 'Inactive']);
749
+ await xml.setKeyField('projects', 'name');
750
+
751
+ // Field aliases in importFromCSV
752
+ await xml.importFromCSV('projects', {
753
+ recordsCsv: 'Project A,Active',
754
+ clist: ['name', 'status'],
755
+ mergeFieldId: 'id',
756
+ });
757
+ ```
758
+
759
+ **Response Transformation:**
760
+
761
+ The XML client transforms responses to use field and table aliases as keys when a schema is configured, similar to the JSON API:
762
+
763
+ ```typescript
764
+ // getRecordInfo - access fields by alias
765
+ const record = await xml.getRecordInfo('projects', 123);
766
+ console.log(record.fields.name.value); // "Project Alpha"
767
+ console.log(record.fields.status.value); // "Active"
768
+
769
+ // Without schema, fields are keyed by ID
770
+ const record2 = await xmlNoSchema.getRecordInfo('bqprojects123', 123);
771
+ console.log(record2.fields['6'].value); // "Project Alpha"
772
+
773
+ // getSchema - access fields by alias
774
+ const schema = await xml.getSchema('projects');
775
+ console.log(schema.table.fields?.name.label); // "Project Name"
776
+ console.log(schema.table.fields?.status.label); // "Status"
777
+ console.log(schema.table.childTables?.tasks.dbid); // "bqtasks123"
778
+
779
+ // grantedDBs - access databases by alias
780
+ const dbs = await xml.grantedDBs();
781
+ console.log(dbs.databases.projects.dbname); // "My Projects App"
782
+ console.log(dbs.databases.tasks.dbname); // "Tasks Table"
783
+
784
+ // Unknown tables/fields are keyed by their ID
785
+ console.log(dbs.databases['bqunknown123'].dbname); // "Unknown Table"
786
+
787
+ // getAppDTMInfo - access tables by alias
788
+ const dtm = await xml.getAppDTMInfo('projects');
789
+ console.log(dtm.appAlias); // "projects"
790
+ console.log(dtm.tables.projects.lastModifiedTime); // "1234567890"
791
+ console.log(dtm.tables.tasks.lastModifiedTime); // "1234567891"
792
+ ```
793
+
794
+ **Responses transformed to keyed objects:**
795
+
796
+ | Method | Keyed Properties |
797
+ |--------|-----------------|
798
+ | `getRecordInfo` / `getRecordInfoByKey` | `fields.{alias}` |
799
+ | `getSchema` | `table.fields.{alias}`, `table.childTables.{alias}` |
800
+ | `grantedDBs` / `grantedDBsForGroup` | `databases.{alias}` |
801
+ | `getAppDTMInfo` | `tables.{alias}` |
802
+ | `fieldAddChoices` / `fieldRemoveChoices` | `fieldAlias` property |
803
+ | `findDBByName` | `alias` property |
804
+
315
805
  ## API Methods
316
806
 
317
807
  All QuickBase API endpoints are available as typed methods:
318
808
 
319
809
  ```typescript
320
810
  // Apps
321
- const app = await client.getApp({ appId: 'bpqe82s1' });
322
- const newApp = await client.createApp({ name: 'My App', description: 'Test' });
323
- await client.updateApp({ appId: 'bpqe82s1' }, { name: 'Updated Name' });
324
- await client.deleteApp({ appId: 'bpqe82s1' }, { name: 'My App' });
811
+ const app = await qb.getApp({ appId: 'bpqe82s1' });
812
+ const newApp = await qb.createApp({ name: 'My App', description: 'Test' });
813
+ await qb.updateApp({ appId: 'bpqe82s1' }, { name: 'Updated Name' });
814
+ await qb.deleteApp({ appId: 'bpqe82s1' }, { name: 'My App' });
325
815
 
326
816
  // Tables
327
- const tables = await client.getAppTables({ appId: 'bpqe82s1' });
328
- const table = await client.getTable({ appId: 'bpqe82s1', tableId: 'byyy82s1' });
817
+ const tables = await qb.getAppTables({ appId: 'bpqe82s1' });
818
+ const table = await qb.getTable({ appId: 'bpqe82s1', tableId: 'byyy82s1' });
329
819
 
330
820
  // Fields
331
- const fields = await client.getFields({ tableId: 'byyy82s1' });
821
+ const fields = await qb.getFields({ tableId: 'byyy82s1' });
332
822
 
333
823
  // Records
334
- const records = await client.runQuery({
824
+ const records = await qb.runQuery({
335
825
  from: 'byyy82s1',
336
826
  select: [3, 6, 7],
337
827
  where: "{6.GT.100}",
@@ -339,28 +829,28 @@ const records = await client.runQuery({
339
829
  options: { top: 100, skip: 0 },
340
830
  });
341
831
 
342
- // Insert/Update records
343
- const result = await client.upsert({
832
+ // Insert/Update records (values are auto-wrapped)
833
+ const result = await qb.upsert({
344
834
  to: 'byyy82s1',
345
835
  data: [
346
- { '6': { value: 'New Record' }, '7': { value: 42 } },
836
+ { '6': 'New Record', '7': 42 },
347
837
  ],
348
838
  });
349
839
 
350
840
  // Delete records
351
- await client.deleteRecords({
841
+ await qb.deleteRecords({
352
842
  from: 'byyy82s1',
353
843
  where: "{3.EX.123}",
354
844
  });
355
845
 
356
846
  // Reports
357
- const report = await client.runReport(
847
+ const report = await qb.runReport(
358
848
  { reportId: 'abc123', tableId: 'byyy82s1' },
359
849
  { skip: 0, top: 100 }
360
850
  );
361
851
 
362
852
  // Users
363
- const users = await client.getUsers(
853
+ const users = await qb.getUsers(
364
854
  { accountId: '123456' },
365
855
  { appIds: ['bpqe82s1'] }
366
856
  );
@@ -371,7 +861,7 @@ const users = await client.getUsers(
371
861
  For endpoints not covered by typed methods, use the generic `request` method:
372
862
 
373
863
  ```typescript
374
- const result = await client.request<MyResponseType>({
864
+ const result = await qb.request<MyResponseType>({
375
865
  method: 'POST',
376
866
  path: '/some/endpoint',
377
867
  body: { key: 'value' },
@@ -379,6 +869,99 @@ const result = await client.request<MyResponseType>({
379
869
  });
380
870
  ```
381
871
 
872
+ ## Read-Only Mode
873
+
874
+ 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.
875
+
876
+ ```typescript
877
+ import { createClient, createXmlClient, ReadOnlyError } from 'quickbase-js';
878
+
879
+ const qb = createClient({
880
+ realm: 'mycompany',
881
+ auth: { type: 'user-token', userToken: 'token' },
882
+ readOnly: true,
883
+ });
884
+
885
+ // Read operations work normally
886
+ const app = await qb.getApp({ appId: 'bpqe82s1' });
887
+ const records = await qb.runQuery({ from: 'byyy82s1' });
888
+
889
+ // Write operations throw ReadOnlyError
890
+ try {
891
+ await qb.upsert({ to: 'byyy82s1', data: [{ '6': 'test' }] });
892
+ } catch (error) {
893
+ if (error instanceof ReadOnlyError) {
894
+ console.log(`Blocked: ${error.method} ${error.path}`);
895
+ // "Blocked: POST /records"
896
+ }
897
+ }
898
+ ```
899
+
900
+ ### What Gets Blocked
901
+
902
+ | Operation Type | Examples | Blocked? |
903
+ |----------------|----------|----------|
904
+ | **GET requests** | getApp, getFields, getTables | ✅ Allowed |
905
+ | **Query POSTs** | runQuery, runReport, getUsers | ✅ Allowed |
906
+ | **Create/Update** | upsert, createApp, createTable | ❌ Blocked |
907
+ | **Delete** | deleteRecords, deleteApp | ❌ Blocked |
908
+ | **XML writes** | addUserToRole, setDBVar | ❌ Blocked |
909
+
910
+ ### Defense-in-Depth
911
+
912
+ The read-only check uses two layers:
913
+
914
+ 1. **Explicit blocklist** - Known write endpoints are blocked by path matching
915
+ 2. **HTTP method check** - Any POST/PUT/DELETE/PATCH not in the allowlist is blocked
916
+
917
+ This ensures new API endpoints are blocked by default until explicitly allowlisted.
918
+
919
+ ### XML API
920
+
921
+ The XML client automatically inherits read-only mode from the main client:
922
+
923
+ ```typescript
924
+ const qb = createClient({
925
+ realm: 'mycompany',
926
+ auth: { type: 'user-token', userToken: 'token' },
927
+ readOnly: true,
928
+ });
929
+
930
+ const xml = createXmlClient(qb); // Inherits readOnly: true
931
+
932
+ // Read operations work
933
+ const roles = await xml.getRoleInfo(appId);
934
+
935
+ // Write operations throw ReadOnlyError with action info
936
+ try {
937
+ await xml.setDBVar(appId, 'myVar', 'value');
938
+ } catch (error) {
939
+ if (error instanceof ReadOnlyError) {
940
+ console.log(`Blocked XML action: ${error.action}`);
941
+ // "Blocked XML action: API_SetDBVar"
942
+ }
943
+ }
944
+ ```
945
+
946
+ ### ReadOnlyError
947
+
948
+ The `ReadOnlyError` class provides details about the blocked operation:
949
+
950
+ ```typescript
951
+ import { ReadOnlyError } from 'quickbase-js';
952
+
953
+ try {
954
+ await qb.deleteRecords({ from: 'byyy82s1', where: '{3.GT.0}' });
955
+ } catch (error) {
956
+ if (error instanceof ReadOnlyError) {
957
+ error.method; // "DELETE"
958
+ error.path; // "/records"
959
+ error.action; // undefined (only set for XML API)
960
+ error.message; // "Read-only mode: write operation blocked (DELETE /records)"
961
+ }
962
+ }
963
+ ```
964
+
382
965
  ## Error Handling
383
966
 
384
967
  ```typescript
@@ -390,10 +973,11 @@ import {
390
973
  NotFoundError,
391
974
  ValidationError,
392
975
  ServerError,
976
+ ReadOnlyError,
393
977
  } from 'quickbase-js';
394
978
 
395
979
  try {
396
- await client.getApp({ appId: 'invalid' });
980
+ await qb.getApp({ appId: 'invalid' });
397
981
  } catch (error) {
398
982
  if (error instanceof RateLimitError) {
399
983
  console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
@@ -445,7 +1029,7 @@ Throw RateLimitError if exhausted
445
1029
  ### Retry Configuration
446
1030
 
447
1031
  ```typescript
448
- const client = createClient({
1032
+ const qb = createClient({
449
1033
  realm: 'mycompany',
450
1034
  auth: { type: 'user-token', userToken: 'token' },
451
1035
  rateLimit: {
@@ -466,7 +1050,7 @@ The backoff formula with jitter: `delay = initialDelayMs * (multiplier ^ attempt
466
1050
  Prevent 429 errors entirely by throttling requests client-side using a sliding window algorithm:
467
1051
 
468
1052
  ```typescript
469
- const client = createClient({
1053
+ const qb = createClient({
470
1054
  realm: 'mycompany',
471
1055
  auth: { type: 'user-token', userToken: 'token' },
472
1056
  rateLimit: {
@@ -485,7 +1069,7 @@ This tracks request timestamps and blocks new requests when the limit would be e
485
1069
  Get notified when rate limited (called before retry):
486
1070
 
487
1071
  ```typescript
488
- const client = createClient({
1072
+ const qb = createClient({
489
1073
  realm: 'mycompany',
490
1074
  auth: { type: 'user-token', userToken: 'token' },
491
1075
  rateLimit: {
@@ -507,7 +1091,7 @@ If retries are exhausted, a `RateLimitError` is thrown:
507
1091
  import { RateLimitError } from 'quickbase-js';
508
1092
 
509
1093
  try {
510
- await client.getApp({ appId: 'bpqe82s1' });
1094
+ await qb.getApp({ appId: 'bpqe82s1' });
511
1095
  } catch (error) {
512
1096
  if (error instanceof RateLimitError) {
513
1097
  console.log(`Rate limited after ${error.rateLimitInfo.attempt} attempts`);
@@ -525,7 +1109,7 @@ try {
525
1109
  const qb = quickbase({ realm: 'company', userToken: 'token' });
526
1110
 
527
1111
  // v2 - user token (explicit type)
528
- const client = createClient({
1112
+ const qb = createClient({
529
1113
  realm: 'company',
530
1114
  auth: { type: 'user-token', userToken: 'token' },
531
1115
  });
@@ -534,7 +1118,7 @@ const client = createClient({
534
1118
  const qb = quickbase({ realm: 'company', useTempTokens: true });
535
1119
 
536
1120
  // v2 - temp tokens in Code Pages
537
- const client = createClient({
1121
+ const qb = createClient({
538
1122
  realm: 'company',
539
1123
  auth: { type: 'temp-token' },
540
1124
  });
@@ -548,8 +1132,8 @@ await qb.withPaginationDisabled(async () => qb.runQuery({ body: { from: tableId
548
1132
  await qb.withPaginationLimit(100, async () => qb.runQuery({ body: { from: tableId } }));
549
1133
 
550
1134
  // v2 - fluent API
551
- await client.runQuery({ from: tableId }).noPaginate();
552
- await client.runQuery({ from: tableId }).paginate({ limit: 100 });
1135
+ await qb.runQuery({ from: tableId }).noPaginate();
1136
+ await qb.runQuery({ from: tableId }).paginate({ limit: 100 });
553
1137
  ```
554
1138
 
555
1139
  ### Request Body
@@ -559,7 +1143,7 @@ await client.runQuery({ from: tableId }).paginate({ limit: 100 });
559
1143
  await qb.runQuery({ body: { from: tableId, select: [3, 6] } });
560
1144
 
561
1145
  // v2 - direct parameters
562
- await client.runQuery({ from: tableId, select: [3, 6] });
1146
+ await qb.runQuery({ from: tableId, select: [3, 6] });
563
1147
  ```
564
1148
 
565
1149
  ## Development