quickbase-js 2.0.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.
- package/README.md +804 -59
- package/bin/quickbase-js.js +54 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +9 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/ticket.d.ts +84 -0
- package/dist/auth/ticket.d.ts.map +1 -0
- package/dist/auth/ticket.js +260 -0
- package/dist/auth/ticket.js.map +1 -0
- package/dist/cli/schema.d.ts +23 -0
- package/dist/cli/schema.d.ts.map +1 -0
- package/dist/cli/schema.js +425 -0
- package/dist/cli/schema.js.map +1 -0
- package/dist/client/request.d.ts +2 -1
- package/dist/client/request.d.ts.map +1 -1
- package/dist/client/request.js +165 -18
- package/dist/client/request.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/errors.d.ts +17 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +32 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/schema.d.ts +37 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +168 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/transform.d.ts +36 -0
- package/dist/core/transform.d.ts.map +1 -0
- package/dist/core/transform.js +205 -0
- package/dist/core/transform.js.map +1 -0
- package/dist/core/types.d.ts +104 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/quickbase.esm.js +6 -2
- package/dist/quickbase.esm.js.map +4 -4
- package/dist/quickbase.min.js +6 -2
- package/dist/quickbase.min.js.map +4 -4
- package/dist/xml/codepages.d.ts +60 -0
- package/dist/xml/codepages.d.ts.map +1 -0
- package/dist/xml/codepages.js +88 -0
- package/dist/xml/codepages.js.map +1 -0
- package/dist/xml/dbvars.d.ts +39 -0
- package/dist/xml/dbvars.d.ts.map +1 -0
- package/dist/xml/dbvars.js +54 -0
- package/dist/xml/dbvars.js.map +1 -0
- package/dist/xml/errors.d.ts +64 -0
- package/dist/xml/errors.d.ts.map +1 -0
- package/dist/xml/errors.js +102 -0
- package/dist/xml/errors.js.map +1 -0
- package/dist/xml/fields.d.ts +45 -0
- package/dist/xml/fields.d.ts.map +1 -0
- package/dist/xml/fields.js +86 -0
- package/dist/xml/fields.js.map +1 -0
- package/dist/xml/groups.d.ts +196 -0
- package/dist/xml/groups.d.ts.map +1 -0
- package/dist/xml/groups.js +424 -0
- package/dist/xml/groups.js.map +1 -0
- package/dist/xml/html.d.ts +61 -0
- package/dist/xml/html.d.ts.map +1 -0
- package/dist/xml/html.js +110 -0
- package/dist/xml/html.js.map +1 -0
- package/dist/xml/index.d.ts +453 -0
- package/dist/xml/index.d.ts.map +1 -0
- package/dist/xml/index.js +741 -0
- package/dist/xml/index.js.map +1 -0
- package/dist/xml/metadata.d.ts +49 -0
- package/dist/xml/metadata.d.ts.map +1 -0
- package/dist/xml/metadata.js +84 -0
- package/dist/xml/metadata.js.map +1 -0
- package/dist/xml/records.d.ts +106 -0
- package/dist/xml/records.d.ts.map +1 -0
- package/dist/xml/records.js +220 -0
- package/dist/xml/records.js.map +1 -0
- package/dist/xml/request.d.ts +115 -0
- package/dist/xml/request.d.ts.map +1 -0
- package/dist/xml/request.js +167 -0
- package/dist/xml/request.js.map +1 -0
- package/dist/xml/roles.d.ts +117 -0
- package/dist/xml/roles.d.ts.map +1 -0
- package/dist/xml/roles.js +241 -0
- package/dist/xml/roles.js.map +1 -0
- package/dist/xml/schema.d.ts +94 -0
- package/dist/xml/schema.d.ts.map +1 -0
- package/dist/xml/schema.js +354 -0
- package/dist/xml/schema.js.map +1 -0
- package/dist/xml/transform.d.ts +154 -0
- package/dist/xml/transform.d.ts.map +1 -0
- package/dist/xml/transform.js +182 -0
- package/dist/xml/transform.js.map +1 -0
- package/dist/xml/types.d.ts +531 -0
- package/dist/xml/types.d.ts.map +1 -0
- package/dist/xml/types.js +22 -0
- package/dist/xml/types.js.map +1 -0
- package/dist/xml/users.d.ts +92 -0
- package/dist/xml/users.d.ts.map +1 -0
- package/dist/xml/users.js +166 -0
- package/dist/xml/users.js.map +1 -0
- package/dist/xml/webhooks.d.ts +98 -0
- package/dist/xml/webhooks.d.ts.map +1 -0
- package/dist/xml/webhooks.js +202 -0
- package/dist/xml/webhooks.js.map +1 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -5,18 +5,21 @@ 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
|
|
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
|
|
|
17
|
-
**[View
|
|
20
|
+
**[View Full API Documentation →](https://drewbradfordxyz.github.io/quickbase-js/)**
|
|
18
21
|
|
|
19
|
-
The SDK provides typed methods for all 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.
|
|
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.
|
|
20
23
|
|
|
21
24
|
## Installation
|
|
22
25
|
|
|
@@ -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
|
|
42
|
+
const qb = QuickBase.createClient({
|
|
40
43
|
realm: realm,
|
|
41
44
|
auth: { type: 'temp-token' },
|
|
42
45
|
});
|
|
43
46
|
|
|
44
|
-
|
|
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
|
|
59
|
+
const qb = createClient({
|
|
57
60
|
realm: 'mycompany',
|
|
58
61
|
auth: { type: 'temp-token' },
|
|
59
62
|
});
|
|
60
63
|
|
|
61
|
-
const app = await
|
|
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
|
|
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
|
|
83
|
+
const app = await qb.getApp({ appId: 'bpqe82s1' });
|
|
81
84
|
console.log(app.name);
|
|
82
85
|
|
|
83
86
|
// Query records
|
|
84
|
-
const records = await
|
|
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
|
|
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.
|
|
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 -
|
|
112
|
-
const
|
|
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
|
|
136
|
+
const qb = createClient({
|
|
144
137
|
realm: 'mycompany',
|
|
145
138
|
auth: {
|
|
146
139
|
type: 'sso',
|
|
@@ -149,6 +142,36 @@ const client = createClient({
|
|
|
149
142
|
});
|
|
150
143
|
```
|
|
151
144
|
|
|
145
|
+
### Ticket Auth (Username/Password)
|
|
146
|
+
|
|
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.
|
|
148
|
+
|
|
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
|
+
},
|
|
164
|
+
});
|
|
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
|
+
|
|
152
175
|
## Pagination
|
|
153
176
|
|
|
154
177
|
Paginated endpoints (`runQuery`, `runReport`, `getUsers`, etc.) return a `PaginatedRequest` that supports fluent pagination methods.
|
|
@@ -157,7 +180,7 @@ Paginated endpoints (`runQuery`, `runReport`, `getUsers`, etc.) return a `Pagina
|
|
|
157
180
|
|
|
158
181
|
```typescript
|
|
159
182
|
// Returns first page only
|
|
160
|
-
const page = await
|
|
183
|
+
const page = await qb.runQuery({ from: 'tableId' });
|
|
161
184
|
console.log(page.data.length); // e.g., 100 records
|
|
162
185
|
console.log(page.metadata.totalRecords); // e.g., 5000 total
|
|
163
186
|
```
|
|
@@ -166,7 +189,7 @@ console.log(page.metadata.totalRecords); // e.g., 5000 total
|
|
|
166
189
|
|
|
167
190
|
```typescript
|
|
168
191
|
// Automatically fetches all pages and combines results
|
|
169
|
-
const all = await
|
|
192
|
+
const all = await qb.runQuery({ from: 'tableId' }).all();
|
|
170
193
|
console.log(all.data.length); // 5000 records
|
|
171
194
|
```
|
|
172
195
|
|
|
@@ -174,7 +197,7 @@ console.log(all.data.length); // 5000 records
|
|
|
174
197
|
|
|
175
198
|
```typescript
|
|
176
199
|
// Fetch up to 500 records (may span multiple pages)
|
|
177
|
-
const limited = await
|
|
200
|
+
const limited = await qb.runQuery({ from: 'tableId' }).paginate({ limit: 500 });
|
|
178
201
|
console.log(limited.data.length); // 500 records
|
|
179
202
|
```
|
|
180
203
|
|
|
@@ -182,23 +205,23 @@ console.log(limited.data.length); // 500 records
|
|
|
182
205
|
|
|
183
206
|
```typescript
|
|
184
207
|
// Explicitly fetch single page (useful when autoPaginate is enabled globally)
|
|
185
|
-
const single = await
|
|
208
|
+
const single = await qb.runQuery({ from: 'tableId' }).noPaginate();
|
|
186
209
|
```
|
|
187
210
|
|
|
188
211
|
### Global Auto-Pagination
|
|
189
212
|
|
|
190
213
|
```typescript
|
|
191
|
-
const
|
|
214
|
+
const qb = createClient({
|
|
192
215
|
realm: 'mycompany',
|
|
193
216
|
auth: { type: 'user-token', userToken: 'token' },
|
|
194
217
|
autoPaginate: true, // Default await now fetches all pages
|
|
195
218
|
});
|
|
196
219
|
|
|
197
220
|
// Now direct await fetches all pages
|
|
198
|
-
const all = await
|
|
221
|
+
const all = await qb.runQuery({ from: 'tableId' });
|
|
199
222
|
|
|
200
223
|
// Use .noPaginate() to get single page
|
|
201
|
-
const page = await
|
|
224
|
+
const page = await qb.runQuery({ from: 'tableId' }).noPaginate();
|
|
202
225
|
```
|
|
203
226
|
|
|
204
227
|
## Date Conversion
|
|
@@ -206,14 +229,14 @@ const page = await client.runQuery({ from: 'tableId' }).noPaginate();
|
|
|
206
229
|
By default, ISO 8601 date strings in API responses are automatically converted to JavaScript `Date` objects:
|
|
207
230
|
|
|
208
231
|
```typescript
|
|
209
|
-
const app = await
|
|
232
|
+
const app = await qb.getApp({ appId: 'bpqe82s1' });
|
|
210
233
|
|
|
211
234
|
// Date fields are now Date objects
|
|
212
235
|
console.log(app.created instanceof Date); // true
|
|
213
236
|
console.log(app.created.toISOString()); // "2024-01-15T10:30:00.000Z"
|
|
214
237
|
|
|
215
238
|
// Works with nested objects and arrays
|
|
216
|
-
const records = await
|
|
239
|
+
const records = await qb.runQuery({ from: 'tableId' });
|
|
217
240
|
records.data.forEach(record => {
|
|
218
241
|
// Date field values are converted
|
|
219
242
|
console.log(record['7'].value instanceof Date); // true for date/timestamp fields
|
|
@@ -223,17 +246,221 @@ records.data.forEach(record => {
|
|
|
223
246
|
To disable date conversion and keep dates as strings:
|
|
224
247
|
|
|
225
248
|
```typescript
|
|
226
|
-
const
|
|
249
|
+
const qb = createClient({
|
|
227
250
|
realm: 'mycompany',
|
|
228
251
|
auth: { type: 'user-token', userToken: 'token' },
|
|
229
252
|
convertDates: false, // Keep dates as ISO strings
|
|
230
253
|
});
|
|
231
254
|
```
|
|
232
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
|
+
|
|
233
460
|
## Configuration Options
|
|
234
461
|
|
|
235
462
|
```typescript
|
|
236
|
-
const
|
|
463
|
+
const qb = createClient({
|
|
237
464
|
// Required
|
|
238
465
|
realm: 'mycompany',
|
|
239
466
|
auth: { /* see authentication section */ },
|
|
@@ -244,6 +471,8 @@ const client = createClient({
|
|
|
244
471
|
baseUrl: 'https://api.quickbase.com/v1', // API base URL
|
|
245
472
|
autoPaginate: false, // Auto-paginate on direct await
|
|
246
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
|
|
247
476
|
|
|
248
477
|
// Rate limiting
|
|
249
478
|
rateLimit: {
|
|
@@ -272,26 +501,327 @@ const client = createClient({
|
|
|
272
501
|
});
|
|
273
502
|
```
|
|
274
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
|
+
|
|
275
805
|
## API Methods
|
|
276
806
|
|
|
277
807
|
All QuickBase API endpoints are available as typed methods:
|
|
278
808
|
|
|
279
809
|
```typescript
|
|
280
810
|
// Apps
|
|
281
|
-
const app = await
|
|
282
|
-
const newApp = await
|
|
283
|
-
await
|
|
284
|
-
await
|
|
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' });
|
|
285
815
|
|
|
286
816
|
// Tables
|
|
287
|
-
const tables = await
|
|
288
|
-
const table = await
|
|
817
|
+
const tables = await qb.getAppTables({ appId: 'bpqe82s1' });
|
|
818
|
+
const table = await qb.getTable({ appId: 'bpqe82s1', tableId: 'byyy82s1' });
|
|
289
819
|
|
|
290
820
|
// Fields
|
|
291
|
-
const fields = await
|
|
821
|
+
const fields = await qb.getFields({ tableId: 'byyy82s1' });
|
|
292
822
|
|
|
293
823
|
// Records
|
|
294
|
-
const records = await
|
|
824
|
+
const records = await qb.runQuery({
|
|
295
825
|
from: 'byyy82s1',
|
|
296
826
|
select: [3, 6, 7],
|
|
297
827
|
where: "{6.GT.100}",
|
|
@@ -299,28 +829,28 @@ const records = await client.runQuery({
|
|
|
299
829
|
options: { top: 100, skip: 0 },
|
|
300
830
|
});
|
|
301
831
|
|
|
302
|
-
// Insert/Update records
|
|
303
|
-
const result = await
|
|
832
|
+
// Insert/Update records (values are auto-wrapped)
|
|
833
|
+
const result = await qb.upsert({
|
|
304
834
|
to: 'byyy82s1',
|
|
305
835
|
data: [
|
|
306
|
-
{ '6':
|
|
836
|
+
{ '6': 'New Record', '7': 42 },
|
|
307
837
|
],
|
|
308
838
|
});
|
|
309
839
|
|
|
310
840
|
// Delete records
|
|
311
|
-
await
|
|
841
|
+
await qb.deleteRecords({
|
|
312
842
|
from: 'byyy82s1',
|
|
313
843
|
where: "{3.EX.123}",
|
|
314
844
|
});
|
|
315
845
|
|
|
316
846
|
// Reports
|
|
317
|
-
const report = await
|
|
847
|
+
const report = await qb.runReport(
|
|
318
848
|
{ reportId: 'abc123', tableId: 'byyy82s1' },
|
|
319
849
|
{ skip: 0, top: 100 }
|
|
320
850
|
);
|
|
321
851
|
|
|
322
852
|
// Users
|
|
323
|
-
const users = await
|
|
853
|
+
const users = await qb.getUsers(
|
|
324
854
|
{ accountId: '123456' },
|
|
325
855
|
{ appIds: ['bpqe82s1'] }
|
|
326
856
|
);
|
|
@@ -331,7 +861,7 @@ const users = await client.getUsers(
|
|
|
331
861
|
For endpoints not covered by typed methods, use the generic `request` method:
|
|
332
862
|
|
|
333
863
|
```typescript
|
|
334
|
-
const result = await
|
|
864
|
+
const result = await qb.request<MyResponseType>({
|
|
335
865
|
method: 'POST',
|
|
336
866
|
path: '/some/endpoint',
|
|
337
867
|
body: { key: 'value' },
|
|
@@ -339,6 +869,99 @@ const result = await client.request<MyResponseType>({
|
|
|
339
869
|
});
|
|
340
870
|
```
|
|
341
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
|
+
|
|
342
965
|
## Error Handling
|
|
343
966
|
|
|
344
967
|
```typescript
|
|
@@ -350,10 +973,11 @@ import {
|
|
|
350
973
|
NotFoundError,
|
|
351
974
|
ValidationError,
|
|
352
975
|
ServerError,
|
|
976
|
+
ReadOnlyError,
|
|
353
977
|
} from 'quickbase-js';
|
|
354
978
|
|
|
355
979
|
try {
|
|
356
|
-
await
|
|
980
|
+
await qb.getApp({ appId: 'invalid' });
|
|
357
981
|
} catch (error) {
|
|
358
982
|
if (error instanceof RateLimitError) {
|
|
359
983
|
console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
|
|
@@ -374,6 +998,108 @@ try {
|
|
|
374
998
|
}
|
|
375
999
|
```
|
|
376
1000
|
|
|
1001
|
+
## Rate Limiting
|
|
1002
|
+
|
|
1003
|
+
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.
|
|
1004
|
+
|
|
1005
|
+
### How 429 Errors Are Handled
|
|
1006
|
+
|
|
1007
|
+
When the SDK receives a 429 (Too Many Requests) response, it automatically:
|
|
1008
|
+
|
|
1009
|
+
1. **Extracts rate limit info** from response headers (`Retry-After`, `cf-ray`, `qb-api-ray`)
|
|
1010
|
+
2. **Calls the `onRateLimit` callback** if configured, allowing you to log or monitor
|
|
1011
|
+
3. **Waits before retrying** - uses the `Retry-After` header if present, otherwise exponential backoff with jitter
|
|
1012
|
+
4. **Retries the request** up to `maxAttempts` times (default: 3)
|
|
1013
|
+
5. **Throws a `RateLimitError`** if all retries are exhausted
|
|
1014
|
+
|
|
1015
|
+
```
|
|
1016
|
+
Request fails with 429
|
|
1017
|
+
↓
|
|
1018
|
+
Extract Retry-After header
|
|
1019
|
+
↓
|
|
1020
|
+
Call onRateLimit callback (if set)
|
|
1021
|
+
↓
|
|
1022
|
+
Wait (Retry-After or exponential backoff)
|
|
1023
|
+
↓
|
|
1024
|
+
Retry request (up to maxAttempts)
|
|
1025
|
+
↓
|
|
1026
|
+
Throw RateLimitError if exhausted
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
### Retry Configuration
|
|
1030
|
+
|
|
1031
|
+
```typescript
|
|
1032
|
+
const qb = createClient({
|
|
1033
|
+
realm: 'mycompany',
|
|
1034
|
+
auth: { type: 'user-token', userToken: 'token' },
|
|
1035
|
+
rateLimit: {
|
|
1036
|
+
retry: {
|
|
1037
|
+
maxAttempts: 5, // Default: 3
|
|
1038
|
+
initialDelayMs: 1000, // Default: 1000ms
|
|
1039
|
+
maxDelayMs: 30000, // Default: 30000ms
|
|
1040
|
+
multiplier: 2, // Default: 2
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
The backoff formula with jitter: `delay = initialDelayMs * (multiplier ^ attempt) ± 10%`
|
|
1047
|
+
|
|
1048
|
+
### Proactive Throttling
|
|
1049
|
+
|
|
1050
|
+
Prevent 429 errors entirely by throttling requests client-side using a sliding window algorithm:
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
const qb = createClient({
|
|
1054
|
+
realm: 'mycompany',
|
|
1055
|
+
auth: { type: 'user-token', userToken: 'token' },
|
|
1056
|
+
rateLimit: {
|
|
1057
|
+
proactiveThrottle: {
|
|
1058
|
+
enabled: true,
|
|
1059
|
+
requestsPer10Seconds: 100,
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
This tracks request timestamps and blocks new requests when the limit would be exceeded, waiting until the oldest request exits the 10-second window.
|
|
1066
|
+
|
|
1067
|
+
### Rate Limit Callback
|
|
1068
|
+
|
|
1069
|
+
Get notified when rate limited (called before retry):
|
|
1070
|
+
|
|
1071
|
+
```typescript
|
|
1072
|
+
const qb = createClient({
|
|
1073
|
+
realm: 'mycompany',
|
|
1074
|
+
auth: { type: 'user-token', userToken: 'token' },
|
|
1075
|
+
rateLimit: {
|
|
1076
|
+
onRateLimit: (info) => {
|
|
1077
|
+
console.log(`Rate limited on ${info.requestUrl}`);
|
|
1078
|
+
console.log(`Retry after: ${info.retryAfter} seconds`);
|
|
1079
|
+
console.log(`Ray ID: ${info.qbApiRay}`);
|
|
1080
|
+
console.log(`Attempt: ${info.attempt}`);
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
});
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### Handling RateLimitError
|
|
1087
|
+
|
|
1088
|
+
If retries are exhausted, a `RateLimitError` is thrown:
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
import { RateLimitError } from 'quickbase-js';
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
await qb.getApp({ appId: 'bpqe82s1' });
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
if (error instanceof RateLimitError) {
|
|
1097
|
+
console.log(`Rate limited after ${error.rateLimitInfo.attempt} attempts`);
|
|
1098
|
+
console.log(`Retry after: ${error.retryAfter} seconds`);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
```
|
|
1102
|
+
|
|
377
1103
|
## Migration from v1
|
|
378
1104
|
|
|
379
1105
|
### Authentication
|
|
@@ -383,7 +1109,7 @@ try {
|
|
|
383
1109
|
const qb = quickbase({ realm: 'company', userToken: 'token' });
|
|
384
1110
|
|
|
385
1111
|
// v2 - user token (explicit type)
|
|
386
|
-
const
|
|
1112
|
+
const qb = createClient({
|
|
387
1113
|
realm: 'company',
|
|
388
1114
|
auth: { type: 'user-token', userToken: 'token' },
|
|
389
1115
|
});
|
|
@@ -392,7 +1118,7 @@ const client = createClient({
|
|
|
392
1118
|
const qb = quickbase({ realm: 'company', useTempTokens: true });
|
|
393
1119
|
|
|
394
1120
|
// v2 - temp tokens in Code Pages
|
|
395
|
-
const
|
|
1121
|
+
const qb = createClient({
|
|
396
1122
|
realm: 'company',
|
|
397
1123
|
auth: { type: 'temp-token' },
|
|
398
1124
|
});
|
|
@@ -406,8 +1132,8 @@ await qb.withPaginationDisabled(async () => qb.runQuery({ body: { from: tableId
|
|
|
406
1132
|
await qb.withPaginationLimit(100, async () => qb.runQuery({ body: { from: tableId } }));
|
|
407
1133
|
|
|
408
1134
|
// v2 - fluent API
|
|
409
|
-
await
|
|
410
|
-
await
|
|
1135
|
+
await qb.runQuery({ from: tableId }).noPaginate();
|
|
1136
|
+
await qb.runQuery({ from: tableId }).paginate({ limit: 100 });
|
|
411
1137
|
```
|
|
412
1138
|
|
|
413
1139
|
### Request Body
|
|
@@ -417,7 +1143,7 @@ await client.runQuery({ from: tableId }).paginate({ limit: 100 });
|
|
|
417
1143
|
await qb.runQuery({ body: { from: tableId, select: [3, 6] } });
|
|
418
1144
|
|
|
419
1145
|
// v2 - direct parameters
|
|
420
|
-
await
|
|
1146
|
+
await qb.runQuery({ from: tableId, select: [3, 6] });
|
|
421
1147
|
```
|
|
422
1148
|
|
|
423
1149
|
## Development
|
|
@@ -445,6 +1171,22 @@ npm run typecheck
|
|
|
445
1171
|
npm run spec:generate
|
|
446
1172
|
```
|
|
447
1173
|
|
|
1174
|
+
### Updating the OpenAPI Spec
|
|
1175
|
+
|
|
1176
|
+
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:
|
|
1177
|
+
|
|
1178
|
+
```bash
|
|
1179
|
+
# Update to latest spec
|
|
1180
|
+
cd spec
|
|
1181
|
+
git pull origin main
|
|
1182
|
+
cd ..
|
|
1183
|
+
git add spec
|
|
1184
|
+
git commit -m "Update quickbase-spec submodule"
|
|
1185
|
+
|
|
1186
|
+
# Regenerate types
|
|
1187
|
+
npm run spec:generate
|
|
1188
|
+
```
|
|
1189
|
+
|
|
448
1190
|
### Spec Tools
|
|
449
1191
|
|
|
450
1192
|
```bash
|
|
@@ -461,14 +1203,17 @@ npm run spec:generate # Generate TypeScript types
|
|
|
461
1203
|
|
|
462
1204
|
### Documentation
|
|
463
1205
|
|
|
464
|
-
|
|
1206
|
+
API documentation is auto-generated from source code and hosted at:
|
|
1207
|
+
**https://drewbradfordxyz.github.io/quickbase-js/**
|
|
1208
|
+
|
|
1209
|
+
To generate docs locally:
|
|
465
1210
|
|
|
466
1211
|
```bash
|
|
467
1212
|
npm run docs # Generate docs to ./docs
|
|
468
1213
|
npm run docs:watch # Watch mode for development
|
|
469
1214
|
```
|
|
470
1215
|
|
|
471
|
-
|
|
1216
|
+
Documentation is built with [TypeDoc](https://typedoc.org/) and auto-deployed to GitHub Pages on every push to main.
|
|
472
1217
|
|
|
473
1218
|
## License
|
|
474
1219
|
|