quickbase-js 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +145 -1107
  2. package/dist/auth/ticket.d.ts.map +1 -1
  3. package/dist/auth/ticket.js +4 -3
  4. package/dist/auth/ticket.js.map +1 -1
  5. package/dist/client/request.d.ts +4 -0
  6. package/dist/client/request.d.ts.map +1 -1
  7. package/dist/client/request.js +15 -3
  8. package/dist/client/request.js.map +1 -1
  9. package/dist/generated/client.d.ts +2 -0
  10. package/dist/generated/client.d.ts.map +1 -1
  11. package/dist/generated/client.js +134 -113
  12. package/dist/generated/client.js.map +1 -1
  13. package/dist/generated/types.d.ts +2700 -2256
  14. package/dist/generated/types.d.ts.map +1 -1
  15. package/dist/generated/types.js +0 -2
  16. package/dist/generated/types.js.map +1 -1
  17. package/dist/quickbase.esm.js +3 -3
  18. package/dist/quickbase.esm.js.map +3 -3
  19. package/dist/quickbase.min.js +3 -3
  20. package/dist/quickbase.min.js.map +3 -3
  21. package/package.json +16 -13
  22. package/src/auth/index.ts +55 -0
  23. package/src/auth/sso-token.ts +143 -0
  24. package/src/auth/temp-token.ts +160 -0
  25. package/src/auth/ticket.ts +329 -0
  26. package/src/auth/token-cache.ts +97 -0
  27. package/src/auth/types.ts +50 -0
  28. package/src/auth/user-token.ts +38 -0
  29. package/src/cli/schema.ts +512 -0
  30. package/src/client/index.ts +36 -0
  31. package/src/client/pagination.ts +337 -0
  32. package/src/client/request.ts +544 -0
  33. package/src/client/retry.ts +169 -0
  34. package/src/client/throttle.ts +105 -0
  35. package/src/core/config.ts +160 -0
  36. package/src/core/dates.ts +63 -0
  37. package/src/core/errors.ts +349 -0
  38. package/src/core/index.ts +50 -0
  39. package/src/core/logger.ts +113 -0
  40. package/src/core/schema.ts +213 -0
  41. package/src/core/transform.ts +295 -0
  42. package/src/core/types.ts +337 -0
  43. package/src/generated/client.ts +1785 -0
  44. package/src/generated/operations.json +672 -0
  45. package/src/generated/types.ts +4884 -0
  46. package/src/index.ts +259 -0
  47. package/src/xml/codepages.ts +119 -0
  48. package/src/xml/dbvars.ts +75 -0
  49. package/src/xml/errors.ts +133 -0
  50. package/src/xml/fields.ts +121 -0
  51. package/src/xml/groups.ts +560 -0
  52. package/src/xml/html.ts +152 -0
  53. package/src/xml/index.ts +1196 -0
  54. package/src/xml/metadata.ts +111 -0
  55. package/src/xml/records.ts +301 -0
  56. package/src/xml/request.ts +238 -0
  57. package/src/xml/roles.ts +321 -0
  58. package/src/xml/schema.ts +436 -0
  59. package/src/xml/transform.ts +381 -0
  60. package/src/xml/types.ts +602 -0
  61. package/src/xml/users.ts +221 -0
  62. package/src/xml/webhooks.ts +260 -0
@@ -0,0 +1,1196 @@
1
+ /**
2
+ * XML API Client Module
3
+ *
4
+ * Provides access to legacy QuickBase XML API endpoints that have no
5
+ * JSON API equivalent (roles, groups, DBVars, code pages, etc.).
6
+ *
7
+ * XML-API: Remove this file when QuickBase discontinues the XML API.
8
+ * To find all XML API code, search for: XML-API
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { createClient } from 'quickbase-js';
13
+ * import { createXmlClient } from 'quickbase-js/xml';
14
+ *
15
+ * const qb = createClient({
16
+ * realm: 'myrealm',
17
+ * auth: { type: 'user-token', userToken: '...' },
18
+ * });
19
+ *
20
+ * const xml = createXmlClient(qb);
21
+ *
22
+ * // Get all roles in an app
23
+ * const roles = await xml.getRoleInfo('bqxyz123');
24
+ * ```
25
+ */
26
+
27
+ // Re-export types
28
+ export * from './types.js';
29
+ export { XmlError, XmlErrorCode, checkError, isUnauthorized, isNotFound, isInvalidTicket, isInvalidInput } from './errors.js';
30
+
31
+ // Import ReadOnlyError for consistent error handling
32
+ import { ReadOnlyError } from '../core/errors.js';
33
+ import type { ResolvedSchema } from '../core/types.js';
34
+ import { resolveTableAlias, resolveFieldAlias } from '../core/schema.js';
35
+ import { buildRequest, escapeXml, injectAppToken, injectUserToken } from './request.js';
36
+
37
+ // Import response transformation functions and types
38
+ import {
39
+ transformGetRecordInfoResult,
40
+ transformGetSchemaResult,
41
+ transformFieldAddChoicesResult,
42
+ transformFieldRemoveChoicesResult,
43
+ transformGrantedDBsResult,
44
+ transformGrantedDBsForGroupResult,
45
+ transformGetAppDTMInfoResult,
46
+ transformFindDBByNameResult,
47
+ type GetRecordInfoResultWithAlias,
48
+ type GetSchemaResultWithAlias,
49
+ type FieldAddChoicesResultWithAlias,
50
+ type FieldRemoveChoicesResultWithAlias,
51
+ type GrantedDBsResultWithAlias,
52
+ type GrantedDBsForGroupResultWithAlias,
53
+ type GetAppDTMInfoResultWithAlias,
54
+ type FindDBByNameResultWithAlias,
55
+ } from './transform.js';
56
+
57
+ // Re-export transformation types for consumers
58
+ export type {
59
+ RecordFieldWithAlias,
60
+ SchemaFieldWithAlias,
61
+ DatabaseInfoWithAlias,
62
+ SchemaChildTableWithAlias,
63
+ GetRecordInfoResultWithAlias,
64
+ GetSchemaResultWithAlias,
65
+ FieldAddChoicesResultWithAlias,
66
+ FieldRemoveChoicesResultWithAlias,
67
+ GrantedDBsResultWithAlias,
68
+ GrantedDBsForGroupResultWithAlias,
69
+ GetAppDTMInfoResultWithAlias,
70
+ FindDBByNameResultWithAlias,
71
+ } from './transform.js';
72
+
73
+ // Import types for XmlClient
74
+ import type { XmlCaller } from './types.js';
75
+ import type {
76
+ GetRoleInfoResult,
77
+ UserRolesResult,
78
+ GetUserRoleResult,
79
+ GetUsersInGroupResult,
80
+ CreateGroupResult,
81
+ GetGroupRoleResult,
82
+ ProvisionUserResult,
83
+ UserInfo,
84
+ DBInfo,
85
+ AddReplaceDBPageResult,
86
+ PageType,
87
+ DoQueryCountResult,
88
+ GrantedGroupsResult,
89
+ GetAncestorInfoResult,
90
+ ImportFromCSVResult,
91
+ CopyMasterDetailResult,
92
+ WebhooksCreateOptions,
93
+ WebhooksEditOptions,
94
+ } from './types.js';
95
+
96
+ // Import endpoint implementations
97
+ import {
98
+ getRoleInfo as getRoleInfoImpl,
99
+ userRoles as userRolesImpl,
100
+ getUserRole as getUserRoleImpl,
101
+ addUserToRole as addUserToRoleImpl,
102
+ removeUserFromRole as removeUserFromRoleImpl,
103
+ changeUserRole as changeUserRoleImpl,
104
+ } from './roles.js';
105
+
106
+ import {
107
+ createGroup as createGroupImpl,
108
+ deleteGroup as deleteGroupImpl,
109
+ copyGroup as copyGroupImpl,
110
+ changeGroupInfo as changeGroupInfoImpl,
111
+ getUsersInGroup as getUsersInGroupImpl,
112
+ addUserToGroup as addUserToGroupImpl,
113
+ removeUserFromGroup as removeUserFromGroupImpl,
114
+ getGroupRole as getGroupRoleImpl,
115
+ addGroupToRole as addGroupToRoleImpl,
116
+ removeGroupFromRole as removeGroupFromRoleImpl,
117
+ grantedGroups as grantedGroupsImpl,
118
+ grantedDBsForGroup as grantedDBsForGroupImpl,
119
+ addSubgroup as addSubgroupImpl,
120
+ removeSubgroup as removeSubgroupImpl,
121
+ } from './groups.js';
122
+
123
+ import {
124
+ getUserInfo as getUserInfoImpl,
125
+ provisionUser as provisionUserImpl,
126
+ sendInvitation as sendInvitationImpl,
127
+ changeManager as changeManagerImpl,
128
+ changeRecordOwner as changeRecordOwnerImpl,
129
+ } from './users.js';
130
+
131
+ import {
132
+ grantedDBs as grantedDBsImpl,
133
+ findDBByName as findDBByNameImpl,
134
+ getDBInfo as getDBInfoImpl,
135
+ getNumRecords as getNumRecordsImpl,
136
+ getSchema as getSchemaImpl,
137
+ } from './schema.js';
138
+
139
+ import {
140
+ getDBVar as getDBVarImpl,
141
+ setDBVar as setDBVarImpl,
142
+ } from './dbvars.js';
143
+
144
+ import {
145
+ getDBPage as getDBPageImpl,
146
+ addReplaceDBPage as addReplaceDBPageImpl,
147
+ } from './codepages.js';
148
+
149
+ import {
150
+ doQueryCount as doQueryCountImpl,
151
+ getRecordInfo as getRecordInfoImpl,
152
+ getRecordInfoByKey as getRecordInfoByKeyImpl,
153
+ importFromCSV as importFromCSVImpl,
154
+ runImport as runImportImpl,
155
+ copyMasterDetail as copyMasterDetailImpl,
156
+ } from './records.js';
157
+
158
+ import {
159
+ webhooksCreate as webhooksCreateImpl,
160
+ webhooksEdit as webhooksEditImpl,
161
+ webhooksDelete as webhooksDeleteImpl,
162
+ webhooksActivate as webhooksActivateImpl,
163
+ webhooksDeactivate as webhooksDeactivateImpl,
164
+ webhooksCopy as webhooksCopyImpl,
165
+ } from './webhooks.js';
166
+
167
+ import {
168
+ fieldAddChoices as fieldAddChoicesImpl,
169
+ fieldRemoveChoices as fieldRemoveChoicesImpl,
170
+ setKeyField as setKeyFieldImpl,
171
+ } from './fields.js';
172
+
173
+ import {
174
+ genAddRecordForm as genAddRecordFormImpl,
175
+ genResultsTable as genResultsTableImpl,
176
+ getRecordAsHTML as getRecordAsHTMLImpl,
177
+ } from './html.js';
178
+
179
+ import {
180
+ getAppDTMInfo as getAppDTMInfoImpl,
181
+ getAncestorInfo as getAncestorInfoImpl,
182
+ signOut as signOutImpl,
183
+ } from './metadata.js';
184
+
185
+ /**
186
+ * Set of XML API actions that modify data.
187
+ * Used to enforce read-only mode.
188
+ */
189
+ const xmlWriteActions = new Set([
190
+ // User/Role management
191
+ 'API_AddUserToRole',
192
+ 'API_RemoveUserFromRole',
193
+ 'API_ChangeUserRole',
194
+ 'API_ProvisionUser',
195
+ 'API_SendInvitation',
196
+ 'API_ChangeManager',
197
+ 'API_ChangeRecordOwner',
198
+
199
+ // Group management
200
+ 'API_CreateGroup',
201
+ 'API_DeleteGroup',
202
+ 'API_AddUserToGroup',
203
+ 'API_RemoveUserFromGroup',
204
+ 'API_AddGroupToRole',
205
+ 'API_RemoveGroupFromRole',
206
+ 'API_CopyGroup',
207
+ 'API_ChangeGroupInfo',
208
+ 'API_AddSubgroup',
209
+ 'API_RemoveSubgroup',
210
+
211
+ // Variables
212
+ 'API_SetDBVar',
213
+
214
+ // Code pages
215
+ 'API_AddReplaceDBPage',
216
+
217
+ // Fields
218
+ 'API_FieldAddChoices',
219
+ 'API_FieldRemoveChoices',
220
+ 'API_SetKeyField',
221
+
222
+ // Webhooks
223
+ 'API_Webhooks_Create',
224
+ 'API_Webhooks_Edit',
225
+ 'API_Webhooks_Delete',
226
+ 'API_Webhooks_Activate',
227
+ 'API_Webhooks_Deactivate',
228
+ 'API_Webhooks_Copy',
229
+
230
+ // Records/Import
231
+ 'API_ImportFromCSV',
232
+ 'API_RunImport',
233
+ 'API_CopyMasterDetail',
234
+ 'API_PurgeRecords',
235
+ 'API_AddRecord',
236
+ 'API_EditRecord',
237
+ 'API_DeleteRecord',
238
+
239
+ // Auth (clears session state)
240
+ 'API_SignOut',
241
+ ]);
242
+
243
+ /**
244
+ * Check if an XML API action modifies data.
245
+ */
246
+ export function isXmlWriteAction(action: string): boolean {
247
+ return xmlWriteActions.has(action);
248
+ }
249
+
250
+ /**
251
+ * Options for creating an XML client
252
+ */
253
+ export interface XmlClientOptions {
254
+ /** Block all write operations (read-only mode) */
255
+ readOnly?: boolean;
256
+ /** Schema for table/field alias resolution (typically inherited from main client) */
257
+ schema?: ResolvedSchema;
258
+ /** App token for apps that require application tokens (XML API only) */
259
+ appToken?: string;
260
+ /**
261
+ * Custom fetch function for direct HTTP requests (e.g., GetAppDTMInfo which
262
+ * bypasses doXml to avoid sending auth tokens). Defaults to globalThis.fetch.
263
+ */
264
+ fetchApi?: typeof globalThis.fetch;
265
+ }
266
+
267
+ /**
268
+ * XML API Client
269
+ *
270
+ * Provides methods for calling legacy QuickBase XML API endpoints.
271
+ * Create using the `createXmlClient()` function.
272
+ */
273
+ export class XmlClient {
274
+ private readonly caller: XmlCaller;
275
+ private readonly readOnly: boolean;
276
+ private readonly schema?: ResolvedSchema;
277
+ private readonly fetchApi: typeof globalThis.fetch;
278
+
279
+ constructor(caller: XmlCaller, options?: XmlClientOptions) {
280
+ this.caller = caller;
281
+ this.readOnly = options?.readOnly ?? false;
282
+ this.schema = options?.schema;
283
+ this.fetchApi = options?.fetchApi ?? globalThis.fetch;
284
+ // Note: appToken is handled by the caller closure, not stored here
285
+ }
286
+
287
+ /**
288
+ * Resolve a table alias to its ID using the configured schema.
289
+ * If no schema is configured or the input is already a table ID, returns unchanged.
290
+ */
291
+ private resolveTable(tableRef: string): string {
292
+ return resolveTableAlias(this.schema, tableRef);
293
+ }
294
+
295
+ /**
296
+ * Resolve a field alias to its ID using the configured schema.
297
+ * If no schema is configured or the input is already a field ID, returns unchanged.
298
+ */
299
+ private resolveField(tableId: string, fieldRef: string | number): number {
300
+ return resolveFieldAlias(this.schema, tableId, fieldRef);
301
+ }
302
+
303
+ /**
304
+ * Check if an action is allowed (not blocked by read-only mode).
305
+ * Throws ReadOnlyError if the action is blocked.
306
+ */
307
+ private checkWriteAllowed(action: string): void {
308
+ if (this.readOnly && isXmlWriteAction(action)) {
309
+ throw new ReadOnlyError('POST', `/db/${this.caller.realm()}`, action);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Execute a raw XML API request.
315
+ * Enforces read-only mode if enabled.
316
+ * This is exposed for direct XML API calls not covered by typed methods.
317
+ */
318
+ async executeXml(dbid: string, action: string, body: string): Promise<string> {
319
+ this.checkWriteAllowed(action);
320
+ return this.caller.doXml(dbid, action, body);
321
+ }
322
+
323
+ /**
324
+ * Get the realm name
325
+ */
326
+ realm(): string {
327
+ return this.caller.realm();
328
+ }
329
+
330
+ // ============================================================================
331
+ // Role Management
332
+ // ============================================================================
333
+
334
+ /**
335
+ * Get all roles defined in an application.
336
+ * @see https://help.quickbase.com/docs/api-getroleinfo
337
+ */
338
+ async getRoleInfo(appId: string): Promise<GetRoleInfoResult> {
339
+ return getRoleInfoImpl(this.caller, this.resolveTable(appId));
340
+ }
341
+
342
+ /**
343
+ * Get all users in an application and their role assignments.
344
+ * @see https://help.quickbase.com/docs/api-userroles
345
+ */
346
+ async userRoles(appId: string): Promise<UserRolesResult> {
347
+ return userRolesImpl(this.caller, this.resolveTable(appId));
348
+ }
349
+
350
+ /**
351
+ * Get roles for a specific user.
352
+ * @see https://help.quickbase.com/docs/api-getuserrole
353
+ */
354
+ async getUserRole(
355
+ appId: string,
356
+ userId?: string,
357
+ includeGroups?: boolean
358
+ ): Promise<GetUserRoleResult> {
359
+ return getUserRoleImpl(this.caller, this.resolveTable(appId), userId, includeGroups);
360
+ }
361
+
362
+ /**
363
+ * Assign a user to a role.
364
+ * @see https://help.quickbase.com/docs/api-addusertorole
365
+ */
366
+ async addUserToRole(appId: string, userId: string, roleId: number): Promise<void> {
367
+ this.checkWriteAllowed('API_AddUserToRole');
368
+ return addUserToRoleImpl(this.caller, this.resolveTable(appId), userId, roleId);
369
+ }
370
+
371
+ /**
372
+ * Remove a user from a role.
373
+ * @see https://help.quickbase.com/docs/api-removeuserfromrole
374
+ */
375
+ async removeUserFromRole(appId: string, userId: string, roleId: number): Promise<void> {
376
+ this.checkWriteAllowed('API_RemoveUserFromRole');
377
+ return removeUserFromRoleImpl(this.caller, this.resolveTable(appId), userId, roleId);
378
+ }
379
+
380
+ /**
381
+ * Change a user's role.
382
+ * @see https://help.quickbase.com/docs/api-changeuserrole
383
+ */
384
+ async changeUserRole(
385
+ appId: string,
386
+ userId: string,
387
+ currentRoleId: number,
388
+ newRoleId?: number
389
+ ): Promise<void> {
390
+ this.checkWriteAllowed('API_ChangeUserRole');
391
+ return changeUserRoleImpl(this.caller, this.resolveTable(appId), userId, currentRoleId, newRoleId);
392
+ }
393
+
394
+ // ============================================================================
395
+ // Group Management
396
+ // ============================================================================
397
+
398
+ /**
399
+ * Create a new group.
400
+ * @see https://help.quickbase.com/docs/api-creategroup
401
+ */
402
+ async createGroup(
403
+ name: string,
404
+ description?: string,
405
+ accountId?: string
406
+ ): Promise<CreateGroupResult> {
407
+ this.checkWriteAllowed('API_CreateGroup');
408
+ return createGroupImpl(this.caller, name, description, accountId);
409
+ }
410
+
411
+ /**
412
+ * Delete a group.
413
+ * @see https://help.quickbase.com/docs/api-deletegroup
414
+ */
415
+ async deleteGroup(groupId: string): Promise<void> {
416
+ this.checkWriteAllowed('API_DeleteGroup');
417
+ return deleteGroupImpl(this.caller, groupId);
418
+ }
419
+
420
+ /**
421
+ * Copy a group.
422
+ * @see https://help.quickbase.com/docs/api-copygroup
423
+ */
424
+ async copyGroup(
425
+ groupId: string,
426
+ name?: string,
427
+ description?: string,
428
+ accountId?: string
429
+ ): Promise<CreateGroupResult> {
430
+ this.checkWriteAllowed('API_CopyGroup');
431
+ return copyGroupImpl(this.caller, groupId, name, description, accountId);
432
+ }
433
+
434
+ /**
435
+ * Change group information.
436
+ * @see https://help.quickbase.com/docs/api-changegroupinfo
437
+ */
438
+ async changeGroupInfo(
439
+ groupId: string,
440
+ name?: string,
441
+ description?: string,
442
+ accountId?: string
443
+ ): Promise<void> {
444
+ this.checkWriteAllowed('API_ChangeGroupInfo');
445
+ return changeGroupInfoImpl(this.caller, groupId, name, description, accountId);
446
+ }
447
+
448
+ /**
449
+ * Get users and managers in a group.
450
+ * @see https://help.quickbase.com/docs/api-getusersingroup
451
+ */
452
+ async getUsersInGroup(groupId: string, includeManagers?: boolean): Promise<GetUsersInGroupResult> {
453
+ return getUsersInGroupImpl(this.caller, groupId, includeManagers);
454
+ }
455
+
456
+ /**
457
+ * Add a user to a group.
458
+ * @see https://help.quickbase.com/docs/api-addusertogroup
459
+ */
460
+ async addUserToGroup(
461
+ groupId: string,
462
+ userId: string,
463
+ allowAdminAccess?: boolean
464
+ ): Promise<void> {
465
+ this.checkWriteAllowed('API_AddUserToGroup');
466
+ return addUserToGroupImpl(this.caller, groupId, userId, allowAdminAccess);
467
+ }
468
+
469
+ /**
470
+ * Remove a user from a group.
471
+ * @see https://help.quickbase.com/docs/api-removeuserfromgroup
472
+ */
473
+ async removeUserFromGroup(groupId: string, userId: string): Promise<void> {
474
+ this.checkWriteAllowed('API_RemoveUserFromGroup');
475
+ return removeUserFromGroupImpl(this.caller, groupId, userId);
476
+ }
477
+
478
+ /**
479
+ * Get roles assigned to a group in an app.
480
+ * @see https://help.quickbase.com/docs/api-getgrouprole
481
+ */
482
+ async getGroupRole(appId: string, groupId: string): Promise<GetGroupRoleResult> {
483
+ return getGroupRoleImpl(this.caller, this.resolveTable(appId), groupId);
484
+ }
485
+
486
+ /**
487
+ * Assign a group to a role.
488
+ * @see https://help.quickbase.com/docs/api-addgrouptorole
489
+ */
490
+ async addGroupToRole(appId: string, groupId: string, roleId: number): Promise<void> {
491
+ this.checkWriteAllowed('API_AddGroupToRole');
492
+ return addGroupToRoleImpl(this.caller, this.resolveTable(appId), groupId, roleId);
493
+ }
494
+
495
+ /**
496
+ * Remove a group from a role.
497
+ * @see https://help.quickbase.com/docs/api-removegroupfromrole
498
+ */
499
+ async removeGroupFromRole(
500
+ appId: string,
501
+ groupId: string,
502
+ roleId: number,
503
+ allRoles?: boolean
504
+ ): Promise<void> {
505
+ this.checkWriteAllowed('API_RemoveGroupFromRole');
506
+ return removeGroupFromRoleImpl(this.caller, this.resolveTable(appId), groupId, roleId, allRoles);
507
+ }
508
+
509
+ /**
510
+ * Get groups a user belongs to.
511
+ * @see https://help.quickbase.com/docs/api-grantedgroups
512
+ */
513
+ async grantedGroups(userId?: string, adminOnly?: boolean): Promise<GrantedGroupsResult> {
514
+ return grantedGroupsImpl(this.caller, userId, adminOnly);
515
+ }
516
+
517
+ /**
518
+ * Get apps a group can access.
519
+ * @see https://help.quickbase.com/docs/api-granteddbsforgroup
520
+ */
521
+ async grantedDBsForGroup(groupId: string): Promise<GrantedDBsForGroupResultWithAlias> {
522
+ const result = await grantedDBsForGroupImpl(this.caller, groupId);
523
+ return transformGrantedDBsForGroupResult(result, this.schema);
524
+ }
525
+
526
+ /**
527
+ * Add a subgroup to a group.
528
+ * @see https://help.quickbase.com/docs/api-addsubgroup
529
+ */
530
+ async addSubgroup(groupId: string, subgroupId: string): Promise<void> {
531
+ this.checkWriteAllowed('API_AddSubgroup');
532
+ return addSubgroupImpl(this.caller, groupId, subgroupId);
533
+ }
534
+
535
+ /**
536
+ * Remove a subgroup from a group.
537
+ * @see https://help.quickbase.com/docs/api-removesubgroup
538
+ */
539
+ async removeSubgroup(groupId: string, subgroupId: string): Promise<void> {
540
+ this.checkWriteAllowed('API_RemoveSubgroup');
541
+ return removeSubgroupImpl(this.caller, groupId, subgroupId);
542
+ }
543
+
544
+ // ============================================================================
545
+ // User Management
546
+ // ============================================================================
547
+
548
+ /**
549
+ * Get user information by email.
550
+ * @see https://help.quickbase.com/docs/api-getuserinfo
551
+ */
552
+ async getUserInfo(email?: string): Promise<UserInfo> {
553
+ return getUserInfoImpl(this.caller, email);
554
+ }
555
+
556
+ /**
557
+ * Create a new unregistered user.
558
+ * @see https://help.quickbase.com/docs/api-provisionuser
559
+ */
560
+ async provisionUser(
561
+ appId: string,
562
+ email: string,
563
+ firstName: string,
564
+ lastName: string,
565
+ roleId?: number
566
+ ): Promise<ProvisionUserResult> {
567
+ this.checkWriteAllowed('API_ProvisionUser');
568
+ return provisionUserImpl(this.caller, this.resolveTable(appId), email, firstName, lastName, roleId);
569
+ }
570
+
571
+ /**
572
+ * Send invitation email to a user.
573
+ * @see https://help.quickbase.com/docs/api-sendinvitation
574
+ */
575
+ async sendInvitation(appId: string, userId: string, userText?: string): Promise<void> {
576
+ this.checkWriteAllowed('API_SendInvitation');
577
+ return sendInvitationImpl(this.caller, this.resolveTable(appId), userId, userText);
578
+ }
579
+
580
+ /**
581
+ * Change the application manager.
582
+ * @see https://help.quickbase.com/docs/api-changemanager
583
+ */
584
+ async changeManager(appId: string, newManagerEmail: string): Promise<void> {
585
+ this.checkWriteAllowed('API_ChangeManager');
586
+ return changeManagerImpl(this.caller, this.resolveTable(appId), newManagerEmail);
587
+ }
588
+
589
+ /**
590
+ * Change the owner of a record.
591
+ * @see https://help.quickbase.com/docs/api-changerecordowner
592
+ */
593
+ async changeRecordOwner(
594
+ tableId: string,
595
+ recordId: number,
596
+ newOwner: string
597
+ ): Promise<void> {
598
+ this.checkWriteAllowed('API_ChangeRecordOwner');
599
+ return changeRecordOwnerImpl(this.caller, this.resolveTable(tableId), recordId, newOwner);
600
+ }
601
+
602
+ // ============================================================================
603
+ // App Discovery
604
+ // ============================================================================
605
+
606
+ /**
607
+ * List all apps/tables the user can access.
608
+ * @see https://help.quickbase.com/docs/api-granteddbs
609
+ */
610
+ async grantedDBs(options?: {
611
+ parentsOnly?: boolean;
612
+ excludeParents?: boolean;
613
+ adminOnly?: boolean;
614
+ includeAncestors?: boolean;
615
+ withEmbeddedTables?: boolean;
616
+ realmAppsOnly?: boolean;
617
+ }): Promise<GrantedDBsResultWithAlias> {
618
+ const result = await grantedDBsImpl(this.caller, options);
619
+ return transformGrantedDBsResult(result, this.schema);
620
+ }
621
+
622
+ /**
623
+ * Find an app by name.
624
+ * @see https://help.quickbase.com/docs/api-finddbbyname
625
+ */
626
+ async findDBByName(name: string, parentsOnly?: boolean): Promise<FindDBByNameResultWithAlias> {
627
+ const result = await findDBByNameImpl(this.caller, name, parentsOnly);
628
+ return transformFindDBByNameResult(result, this.schema);
629
+ }
630
+
631
+ /**
632
+ * Get app/table metadata.
633
+ * @see https://help.quickbase.com/docs/api-getdbinfo
634
+ */
635
+ async getDBInfo(dbid: string): Promise<DBInfo> {
636
+ return getDBInfoImpl(this.caller, this.resolveTable(dbid));
637
+ }
638
+
639
+ /**
640
+ * Get total record count for a table.
641
+ * @see https://help.quickbase.com/docs/api-getnumrecords
642
+ */
643
+ async getNumRecords(tableId: string): Promise<number> {
644
+ return getNumRecordsImpl(this.caller, this.resolveTable(tableId));
645
+ }
646
+
647
+ // ============================================================================
648
+ // Schema Information
649
+ // ============================================================================
650
+
651
+ /**
652
+ * Get comprehensive app/table metadata.
653
+ * @see https://help.quickbase.com/docs/api-getschema
654
+ */
655
+ async getSchema(dbid: string): Promise<GetSchemaResultWithAlias> {
656
+ const resolvedDbid = this.resolveTable(dbid);
657
+ const result = await getSchemaImpl(this.caller, resolvedDbid);
658
+ return transformGetSchemaResult(result, this.schema, resolvedDbid);
659
+ }
660
+
661
+ // ============================================================================
662
+ // Application Variables (DBVars)
663
+ // ============================================================================
664
+
665
+ /**
666
+ * Get an application variable value.
667
+ * @see https://help.quickbase.com/docs/api-getdbvar
668
+ */
669
+ async getDBVar(appId: string, varName: string): Promise<string> {
670
+ return getDBVarImpl(this.caller, this.resolveTable(appId), varName);
671
+ }
672
+
673
+ /**
674
+ * Set an application variable value.
675
+ * @see https://help.quickbase.com/docs/api-setdbvar
676
+ */
677
+ async setDBVar(appId: string, varName: string, value: string): Promise<void> {
678
+ this.checkWriteAllowed('API_SetDBVar');
679
+ return setDBVarImpl(this.caller, this.resolveTable(appId), varName, value);
680
+ }
681
+
682
+ // ============================================================================
683
+ // Code Pages
684
+ // ============================================================================
685
+
686
+ /**
687
+ * Get stored code page content.
688
+ * @see https://help.quickbase.com/docs/api-getdbpage
689
+ */
690
+ async getDBPage(appId: string, pageIdOrName: string | number): Promise<string> {
691
+ return getDBPageImpl(this.caller, this.resolveTable(appId), pageIdOrName);
692
+ }
693
+
694
+ /**
695
+ * Create or update a code page.
696
+ * @see https://help.quickbase.com/docs/api-addreplacedbpage
697
+ */
698
+ async addReplaceDBPage(
699
+ appId: string,
700
+ pageName: string,
701
+ pageId: number | undefined,
702
+ pageType: PageType,
703
+ pageBody: string
704
+ ): Promise<AddReplaceDBPageResult> {
705
+ this.checkWriteAllowed('API_AddReplaceDBPage');
706
+ return addReplaceDBPageImpl(this.caller, this.resolveTable(appId), pageName, pageId, pageType, pageBody);
707
+ }
708
+
709
+ // ============================================================================
710
+ // Field Management
711
+ // ============================================================================
712
+
713
+ /**
714
+ * Add choices to a multiple-choice field.
715
+ * @see https://help.quickbase.com/docs/api-fieldaddchoices
716
+ */
717
+ async fieldAddChoices(
718
+ tableId: string,
719
+ fieldId: number | string,
720
+ choices: string[]
721
+ ): Promise<FieldAddChoicesResultWithAlias> {
722
+ this.checkWriteAllowed('API_FieldAddChoices');
723
+ const resolvedTableId = this.resolveTable(tableId);
724
+ const resolvedFieldId = this.resolveField(resolvedTableId, fieldId);
725
+ const result = await fieldAddChoicesImpl(this.caller, resolvedTableId, resolvedFieldId, choices);
726
+ return transformFieldAddChoicesResult(result, this.schema, resolvedTableId);
727
+ }
728
+
729
+ /**
730
+ * Remove choices from a multiple-choice field.
731
+ * @see https://help.quickbase.com/docs/api-fieldremovechoices
732
+ */
733
+ async fieldRemoveChoices(
734
+ tableId: string,
735
+ fieldId: number | string,
736
+ choices: string[]
737
+ ): Promise<FieldRemoveChoicesResultWithAlias> {
738
+ this.checkWriteAllowed('API_FieldRemoveChoices');
739
+ const resolvedTableId = this.resolveTable(tableId);
740
+ const resolvedFieldId = this.resolveField(resolvedTableId, fieldId);
741
+ const result = await fieldRemoveChoicesImpl(this.caller, resolvedTableId, resolvedFieldId, choices);
742
+ return transformFieldRemoveChoicesResult(result, this.schema, resolvedTableId);
743
+ }
744
+
745
+ /**
746
+ * Set the key field for a table.
747
+ * @see https://help.quickbase.com/docs/api-setkeyfield
748
+ */
749
+ async setKeyField(tableId: string, fieldId: number | string): Promise<void> {
750
+ this.checkWriteAllowed('API_SetKeyField');
751
+ const resolvedTableId = this.resolveTable(tableId);
752
+ const resolvedFieldId = this.resolveField(resolvedTableId, fieldId);
753
+ return setKeyFieldImpl(this.caller, resolvedTableId, resolvedFieldId);
754
+ }
755
+
756
+ // ============================================================================
757
+ // Record Information
758
+ // ============================================================================
759
+
760
+ /**
761
+ * Get count of matching records without fetching data.
762
+ * @see https://help.quickbase.com/docs/api-doquerycount
763
+ */
764
+ async doQueryCount(tableId: string, query?: string): Promise<DoQueryCountResult> {
765
+ return doQueryCountImpl(this.caller, this.resolveTable(tableId), query);
766
+ }
767
+
768
+ /**
769
+ * Get a record with field metadata.
770
+ * @see https://help.quickbase.com/docs/api-getrecordinfo
771
+ */
772
+ async getRecordInfo(tableId: string, recordId: number): Promise<GetRecordInfoResultWithAlias> {
773
+ const resolvedTableId = this.resolveTable(tableId);
774
+ const result = await getRecordInfoImpl(this.caller, resolvedTableId, recordId);
775
+ return transformGetRecordInfoResult(result, this.schema, resolvedTableId);
776
+ }
777
+
778
+ /**
779
+ * Get a record by key field value.
780
+ * @see https://help.quickbase.com/docs/api-getrecordinfo
781
+ */
782
+ async getRecordInfoByKey(tableId: string, keyValue: string): Promise<GetRecordInfoResultWithAlias> {
783
+ const resolvedTableId = this.resolveTable(tableId);
784
+ const result = await getRecordInfoByKeyImpl(this.caller, resolvedTableId, keyValue);
785
+ return transformGetRecordInfoResult(result, this.schema, resolvedTableId);
786
+ }
787
+
788
+ /**
789
+ * Bulk import/update records from CSV data.
790
+ * @see https://help.quickbase.com/docs/api-importfromcsv
791
+ */
792
+ async importFromCSV(
793
+ tableId: string,
794
+ options: {
795
+ recordsCsv: string;
796
+ clist?: (number | string)[];
797
+ skipFirst?: boolean;
798
+ mergeFieldId?: number | string;
799
+ decimalPercent?: boolean;
800
+ }
801
+ ): Promise<ImportFromCSVResult> {
802
+ this.checkWriteAllowed('API_ImportFromCSV');
803
+ const resolvedTableId = this.resolveTable(tableId);
804
+ // Resolve field aliases in clist and mergeFieldId
805
+ const resolvedOptions = {
806
+ ...options,
807
+ clist: options.clist?.map(f => this.resolveField(resolvedTableId, f)),
808
+ mergeFieldId: options.mergeFieldId !== undefined
809
+ ? this.resolveField(resolvedTableId, options.mergeFieldId)
810
+ : undefined,
811
+ };
812
+ return importFromCSVImpl(this.caller, resolvedTableId, resolvedOptions);
813
+ }
814
+
815
+ /**
816
+ * Execute a saved import definition.
817
+ * @see https://help.quickbase.com/docs/api-runimport
818
+ */
819
+ async runImport(tableId: string, importId: number): Promise<ImportFromCSVResult> {
820
+ this.checkWriteAllowed('API_RunImport');
821
+ return runImportImpl(this.caller, this.resolveTable(tableId), importId);
822
+ }
823
+
824
+ /**
825
+ * Copy a master record with its detail records.
826
+ * @see https://help.quickbase.com/docs/api-copymasterdetail
827
+ */
828
+ async copyMasterDetail(
829
+ tableId: string,
830
+ options: {
831
+ rid: number;
832
+ copyfid?: number | string;
833
+ recurse?: boolean;
834
+ relfids?: (number | string)[];
835
+ }
836
+ ): Promise<CopyMasterDetailResult> {
837
+ this.checkWriteAllowed('API_CopyMasterDetail');
838
+ const resolvedTableId = this.resolveTable(tableId);
839
+ // Resolve field aliases in copyfid and relfids
840
+ const resolvedOptions = {
841
+ ...options,
842
+ copyfid: options.copyfid !== undefined
843
+ ? this.resolveField(resolvedTableId, options.copyfid)
844
+ : undefined,
845
+ relfids: options.relfids?.map(f => this.resolveField(resolvedTableId, f)),
846
+ };
847
+ return copyMasterDetailImpl(this.caller, resolvedTableId, resolvedOptions);
848
+ }
849
+
850
+ // ============================================================================
851
+ // App Metadata
852
+ // ============================================================================
853
+
854
+ /**
855
+ * Get modification timestamps (fast, no auth required).
856
+ *
857
+ * This method bypasses the normal doXml transport and makes a direct
858
+ * unauthenticated HTTP request. The QuickBase docs state "No ticket or
859
+ * app token is required" for this call, and sending a user token causes
860
+ * rejection ("User Tokens cannot be used to call this API").
861
+ *
862
+ * @see https://help.quickbase.com/docs/api-getappdtminfo
863
+ */
864
+ async getAppDTMInfo(appId: string): Promise<GetAppDTMInfoResultWithAlias> {
865
+ const resolvedId = this.resolveTable(appId);
866
+ const inner = `<dbid>${escapeXml(resolvedId)}</dbid>`;
867
+ const body = buildRequest(inner);
868
+ const url = `https://${this.caller.realm()}.quickbase.com/db/main`;
869
+
870
+ // Direct unauthenticated fetch — no user token, no app token
871
+ const response = await this.fetchApi(url, {
872
+ method: 'POST',
873
+ headers: {
874
+ 'Content-Type': 'application/xml',
875
+ 'QUICKBASE-ACTION': 'API_GetAppDTMInfo',
876
+ },
877
+ body,
878
+ credentials: 'omit',
879
+ });
880
+
881
+ const xml = await response.text();
882
+
883
+ // Reuse getAppDTMInfoImpl with a passthrough caller that returns pre-fetched XML
884
+ const result = await getAppDTMInfoImpl(
885
+ { realm: this.caller.realm, doXml: async () => xml },
886
+ resolvedId
887
+ );
888
+
889
+ return transformGetAppDTMInfoResult(result, this.schema);
890
+ }
891
+
892
+ /**
893
+ * Get app copy/template lineage info.
894
+ * @see https://help.quickbase.com/docs/api-getancestorinfo
895
+ */
896
+ async getAncestorInfo(appId: string): Promise<GetAncestorInfoResult> {
897
+ return getAncestorInfoImpl(this.caller, this.resolveTable(appId));
898
+ }
899
+
900
+ // ============================================================================
901
+ // Webhooks
902
+ // ============================================================================
903
+
904
+ /**
905
+ * Create a webhook.
906
+ * @see https://help.quickbase.com/docs/api-webhooks-create
907
+ */
908
+ async webhooksCreate(tableId: string, options: WebhooksCreateOptions): Promise<void> {
909
+ this.checkWriteAllowed('API_Webhooks_Create');
910
+ return webhooksCreateImpl(this.caller, this.resolveTable(tableId), options);
911
+ }
912
+
913
+ /**
914
+ * Edit a webhook.
915
+ * @see https://help.quickbase.com/docs/api-webhooks-edit
916
+ */
917
+ async webhooksEdit(
918
+ tableId: string,
919
+ actionId: string,
920
+ options: WebhooksEditOptions
921
+ ): Promise<void> {
922
+ this.checkWriteAllowed('API_Webhooks_Edit');
923
+ return webhooksEditImpl(this.caller, this.resolveTable(tableId), actionId, options);
924
+ }
925
+
926
+ /**
927
+ * Delete webhooks.
928
+ * @see https://help.quickbase.com/docs/api-webhooks-delete
929
+ */
930
+ async webhooksDelete(tableId: string, actionIds: string[]): Promise<{ numChanged: number }> {
931
+ this.checkWriteAllowed('API_Webhooks_Delete');
932
+ return webhooksDeleteImpl(this.caller, this.resolveTable(tableId), actionIds);
933
+ }
934
+
935
+ /**
936
+ * Activate webhooks.
937
+ * @see https://help.quickbase.com/docs/api-webhooks-activate
938
+ */
939
+ async webhooksActivate(tableId: string, actionIds: string[]): Promise<{ numChanged: number }> {
940
+ this.checkWriteAllowed('API_Webhooks_Activate');
941
+ return webhooksActivateImpl(this.caller, this.resolveTable(tableId), actionIds);
942
+ }
943
+
944
+ /**
945
+ * Deactivate webhooks.
946
+ * @see https://help.quickbase.com/docs/api-webhooks-deactivate
947
+ */
948
+ async webhooksDeactivate(tableId: string, actionIds: string[]): Promise<{ numChanged: number }> {
949
+ this.checkWriteAllowed('API_Webhooks_Deactivate');
950
+ return webhooksDeactivateImpl(this.caller, this.resolveTable(tableId), actionIds);
951
+ }
952
+
953
+ /**
954
+ * Copy a webhook.
955
+ * @see https://help.quickbase.com/docs/api-webhooks-copy
956
+ */
957
+ async webhooksCopy(tableId: string, actionId: string): Promise<{ actionId: string }> {
958
+ this.checkWriteAllowed('API_Webhooks_Copy');
959
+ return webhooksCopyImpl(this.caller, this.resolveTable(tableId), actionId);
960
+ }
961
+
962
+ // ============================================================================
963
+ // HTML Generation
964
+ // ============================================================================
965
+
966
+ /**
967
+ * Generate HTML form for adding a record.
968
+ * @see https://help.quickbase.com/docs/api-genaddrecordform
969
+ */
970
+ async genAddRecordForm(
971
+ tableId: string,
972
+ fields?: Array<{ id: number | string; value: string }>
973
+ ): Promise<string> {
974
+ const resolvedTableId = this.resolveTable(tableId);
975
+ const resolvedFields = fields?.map(f => ({
976
+ id: this.resolveField(resolvedTableId, f.id),
977
+ value: f.value,
978
+ }));
979
+ return genAddRecordFormImpl(this.caller, resolvedTableId, resolvedFields);
980
+ }
981
+
982
+ /**
983
+ * Generate HTML/JS/CSV table of query results.
984
+ * @see https://help.quickbase.com/docs/api-genresultstable
985
+ */
986
+ async genResultsTable(
987
+ tableId: string,
988
+ options?: {
989
+ query?: string;
990
+ clist?: (number | string)[];
991
+ slist?: (number | string)[];
992
+ options?: string;
993
+ format?: 'structured' | 'csv';
994
+ }
995
+ ): Promise<string> {
996
+ const resolvedTableId = this.resolveTable(tableId);
997
+ const resolvedOptions = options ? {
998
+ ...options,
999
+ clist: options.clist?.map(f => this.resolveField(resolvedTableId, f)),
1000
+ slist: options.slist?.map(f => this.resolveField(resolvedTableId, f)),
1001
+ } : undefined;
1002
+ return genResultsTableImpl(this.caller, resolvedTableId, resolvedOptions);
1003
+ }
1004
+
1005
+ /**
1006
+ * Get a record rendered as HTML.
1007
+ * @see https://help.quickbase.com/docs/api-getrecordashtml
1008
+ */
1009
+ async getRecordAsHTML(
1010
+ tableId: string,
1011
+ options: {
1012
+ rid?: number;
1013
+ key?: string;
1014
+ jht?: boolean;
1015
+ dfid?: number | string;
1016
+ }
1017
+ ): Promise<string> {
1018
+ const resolvedTableId = this.resolveTable(tableId);
1019
+ const resolvedOptions = {
1020
+ ...options,
1021
+ dfid: options.dfid !== undefined
1022
+ ? this.resolveField(resolvedTableId, options.dfid)
1023
+ : undefined,
1024
+ };
1025
+ return getRecordAsHTMLImpl(this.caller, resolvedTableId, resolvedOptions);
1026
+ }
1027
+
1028
+ // ============================================================================
1029
+ // Authentication
1030
+ // ============================================================================
1031
+
1032
+ /**
1033
+ * Clear ticket cookie (browser-focused).
1034
+ * @see https://help.quickbase.com/docs/api-signout
1035
+ */
1036
+ async signOut(): Promise<void> {
1037
+ return signOutImpl(this.caller);
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Interface for objects that can execute XML API requests.
1043
+ * Used to adapt the main QuickBase client for XML API calls.
1044
+ */
1045
+ export interface XmlExecutor {
1046
+ /**
1047
+ * Execute an XML API request
1048
+ * @param dbid - The database ID
1049
+ * @param action - The QUICKBASE-ACTION header value
1050
+ * @param body - The XML request body
1051
+ * @returns The raw XML response body
1052
+ */
1053
+ executeXml(dbid: string, action: string, body: string): Promise<string>;
1054
+
1055
+ /**
1056
+ * Get the realm name
1057
+ */
1058
+ realm: string;
1059
+ }
1060
+
1061
+ /**
1062
+ * Adapter to create an XmlCaller from a QuickbaseClient.
1063
+ * This bridges the main client to the XML API client.
1064
+ */
1065
+ interface QuickbaseClientLike {
1066
+ getConfig(): {
1067
+ realm: string;
1068
+ fetchApi: typeof fetch;
1069
+ timeout: number;
1070
+ readOnly?: boolean;
1071
+ schema?: ResolvedSchema;
1072
+ appToken?: string;
1073
+ };
1074
+ /**
1075
+ * Get the auth strategy for XML requests.
1076
+ * This is exposed via getAuthStrategy() method added to the client.
1077
+ */
1078
+ getAuthStrategy?: () => {
1079
+ getToken(dbid?: string): Promise<string>;
1080
+ getAuthorizationHeader(token: string): string;
1081
+ };
1082
+ }
1083
+
1084
+ /**
1085
+ * Create an XML API client from a QuickBase client.
1086
+ *
1087
+ * @param client - The main QuickBase client
1088
+ * @param options - Options for the XML client
1089
+ * @returns XmlClient instance
1090
+ *
1091
+ * @example
1092
+ * ```typescript
1093
+ * const qb = createClient({ realm: 'myrealm', auth: {...} });
1094
+ * const xml = createXmlClient(qb);
1095
+ *
1096
+ * const roles = await xml.getRoleInfo('bqxyz123');
1097
+ * ```
1098
+ */
1099
+ export function createXmlClient(
1100
+ client: QuickbaseClientLike,
1101
+ options?: XmlClientOptions
1102
+ ): XmlClient {
1103
+ const config = client.getConfig();
1104
+
1105
+ // Get auth strategy if available
1106
+ const getAuth = client.getAuthStrategy;
1107
+ if (!getAuth) {
1108
+ throw new Error(
1109
+ 'XML client requires access to auth strategy. ' +
1110
+ 'Make sure you are using a compatible version of the QuickBase client.'
1111
+ );
1112
+ }
1113
+ const auth = getAuth();
1114
+
1115
+ // Resolve app token: explicit option > config
1116
+ const appToken = options?.appToken ?? config.appToken;
1117
+
1118
+ // Create an XmlCaller adapter that makes XML requests
1119
+ const caller: XmlCaller = {
1120
+ realm: () => config.realm,
1121
+ doXml: async (dbid: string, action: string, body: string): Promise<string> => {
1122
+ const url = `https://${config.realm}.quickbase.com/db/${encodeURIComponent(dbid)}`;
1123
+
1124
+ // Inject app token if configured
1125
+ let finalBody = appToken ? injectAppToken(body, appToken) : body;
1126
+
1127
+ // Get auth token for this dbid
1128
+ const token = await auth.getToken(dbid);
1129
+ const authHeader = auth.getAuthorizationHeader(token);
1130
+
1131
+ // Build headers - XML API requires tokens in body, not Authorization header
1132
+ // Check if using user token auth (QB-USER-TOKEN prefix)
1133
+ const headers: Record<string, string> = {
1134
+ 'Content-Type': 'application/xml',
1135
+ 'QUICKBASE-ACTION': action,
1136
+ };
1137
+
1138
+ if (authHeader.startsWith('QB-USER-TOKEN ')) {
1139
+ // User token auth: inject token into XML body instead of header
1140
+ // The XML API requires <usertoken> element, not Authorization header
1141
+ // See: https://help.quickbase.com/docs/api-getdbpage
1142
+ finalBody = injectUserToken(finalBody, token);
1143
+ } else {
1144
+ // Other auth types (temp token, SSO): fall back to header auth
1145
+ // Note: These may not work with XML API endpoints that require body auth
1146
+ headers['Authorization'] = authHeader;
1147
+ }
1148
+
1149
+ // Create abort controller for timeout
1150
+ const controller = new AbortController();
1151
+ const timeoutId = setTimeout(() => controller.abort(), config.timeout);
1152
+
1153
+ try {
1154
+ const response = await config.fetchApi(url, {
1155
+ method: 'POST',
1156
+ headers,
1157
+ body: finalBody,
1158
+ signal: controller.signal,
1159
+ credentials: 'omit',
1160
+ });
1161
+
1162
+ const text = await response.text();
1163
+
1164
+ // Check for HTTP-level errors (429, 5xx)
1165
+ if (response.status === 429) {
1166
+ const retryAfter = response.headers.get('Retry-After');
1167
+ throw new Error(
1168
+ `Rate limited (429). Retry after: ${retryAfter ?? 'unknown'} seconds`
1169
+ );
1170
+ }
1171
+
1172
+ if (response.status >= 500) {
1173
+ throw new Error(`Server error: ${response.status} ${response.statusText}`);
1174
+ }
1175
+
1176
+ return text;
1177
+ } catch (error) {
1178
+ if (error instanceof Error && error.name === 'AbortError') {
1179
+ throw new Error(`Request timeout after ${config.timeout}ms`);
1180
+ }
1181
+ throw error;
1182
+ } finally {
1183
+ clearTimeout(timeoutId);
1184
+ }
1185
+ },
1186
+ };
1187
+
1188
+ // Inherit readOnly and schema from main client if not explicitly set in options
1189
+ const resolvedOptions: XmlClientOptions = {
1190
+ readOnly: options?.readOnly ?? config.readOnly ?? false,
1191
+ schema: options?.schema ?? config.schema,
1192
+ fetchApi: options?.fetchApi ?? config.fetchApi,
1193
+ };
1194
+
1195
+ return new XmlClient(caller, resolvedOptions);
1196
+ }