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