quickbase-js 0.1.0-beta.7 → 0.1.0-beta.9

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 (82) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +1292 -125
  4. package/dist/esm/quickbase.js +666 -190
  5. package/dist/esm/quickbase.js.map +1 -1
  6. package/dist/esm/quickbase.min.js +1 -1
  7. package/dist/esm/quickbase.min.js.map +1 -1
  8. package/dist/umd/quickbase.umd.js +674 -189
  9. package/dist/umd/quickbase.umd.js.map +1 -1
  10. package/dist/umd/quickbase.umd.min.js +1 -1
  11. package/dist/umd/quickbase.umd.min.js.map +1 -1
  12. package/open-api/fix-spec-main.ts +69 -68
  13. package/open-api/generate-override-report.ts +214 -0
  14. package/open-api/generate-unified-interface.ts +162 -103
  15. package/open-api/output/override-report.html +11 -0
  16. package/open-api/output/override-report.json +14395 -58
  17. package/open-api/output/quickbase-fixed.json +583 -2
  18. package/open-api/schema/tags/fields.ts +331 -16
  19. package/open-api/schema/tags/index.ts +2 -4
  20. package/open-api/types/spec.ts +2 -1
  21. package/open-api/utils/generateJsDoc.ts +96 -0
  22. package/package.json +33 -33
  23. package/src/RateLimitError.ts +12 -0
  24. package/src/ThrottleBucket.ts +41 -0
  25. package/src/authorizationStrategy.ts +246 -0
  26. package/src/generated-unified/QuickbaseClient.ts +809 -119
  27. package/src/invokeMethod.ts +242 -0
  28. package/src/quickbaseClient.ts +117 -248
  29. package/src/rateLimiter.ts +25 -0
  30. package/src/tokenCache.ts +10 -26
  31. package/tests/setup.ts +2 -0
  32. package/tests/vitest/qb/apps/getApp.test.ts +6 -2
  33. package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
  34. package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
  35. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  36. package/tests/vitest/qb/auth/retryUserToken401.test.ts +73 -0
  37. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  38. package/tests/vitest/qb/fields/createField.test.ts +1 -2
  39. package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
  40. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  41. package/tests/vitest/qb/fields/updateField.test.ts +93 -0
  42. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  43. package/tests/vitest/qb/index.ts +1 -0
  44. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  45. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  46. package/tests/vitest/qb/tables/updateRelationship.test.ts +250 -0
  47. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  48. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  49. package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
  50. package/tests/vitest/unit/apps/updateApp.test.ts +412 -0
  51. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +7 -26
  52. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  53. package/tests/vitest/unit/auth/retryTempToken401.test.ts +95 -0
  54. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  55. package/tests/vitest/unit/auth/retryUserToken401.test.ts +69 -0
  56. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  57. package/tests/vitest/unit/fields/createField.test.ts +180 -120
  58. package/tests/vitest/unit/fields/getFieldUsage.test.ts +47 -14
  59. package/tests/vitest/unit/fields/updateField.test.ts +285 -0
  60. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  61. package/tests/vitest/unit/index.ts +2 -1
  62. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  63. package/tests/vitest/unit/rateLimit.test.ts +191 -0
  64. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  65. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  66. package/tests/vitest/unit/tables/updateRelationship.test.ts +307 -0
  67. package/tests/vitest/unit/tokenLifespan.test.ts +204 -0
  68. package/tools/log-gen.ts +20 -7
  69. package/tools/log-gen.yaml +3446 -1848
  70. package/tsconfig.json +5 -2
  71. package/vitest.config.ts +2 -2
  72. package/notes.txt +0 -158
  73. package/tests/vitest/unit/auth/retryOn401.test.ts +0 -116
  74. package/tests/vitest/unit/auth/two401s.test.ts +0 -86
  75. package/tools/generate-types.ts +0 -29
  76. package/tools/openapi-schema-validator.ts +0 -38
  77. package/tools/spec-analyzer.ts +0 -163
  78. package/tools/split-spec.ts +0 -148
  79. package/tools/test-import.ts +0 -3
  80. package/tools/validate-spec.ts +0 -40
  81. /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
  82. /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
@@ -1,13 +1,12 @@
1
- // tests/vitest/unit/fields/createField.test.ts
2
1
  import { describe, it, expect, beforeEach, vi } from "vitest";
3
2
  import {
4
3
  createClient,
5
4
  mockFetch,
5
+ QB_TABLE_ID_1,
6
6
  QB_REALM,
7
7
  QB_USER_TOKEN,
8
- QB_TABLE_ID_1,
9
8
  } from "@tests/setup.ts";
10
- import { CreateFieldRequest, CreateField200Response } from "@/generated/models";
9
+ import { CreateFieldRequest } from "@/generated/models";
11
10
 
12
11
  describe("QuickbaseClient Unit - createField", () => {
13
12
  let client: ReturnType<typeof createClient>;
@@ -22,52 +21,43 @@ describe("QuickbaseClient Unit - createField", () => {
22
21
  expect(typeof client.createField).toBe("function");
23
22
  });
24
23
 
25
- it("creates a field successfully with user token", async () => {
24
+ it("creates field successfully with user token", async () => {
26
25
  client = createClient(mockFetch, { debug: true });
27
26
 
28
27
  const request: CreateFieldRequest = {
29
- label: "TestField",
28
+ label: "NewField",
30
29
  fieldType: "text",
31
- fieldHelp: "A test field",
32
- addToForms: true,
33
- permissions: [
34
- { role: "Viewer", permissionType: "View", roleId: 10 },
35
- { role: "Administrator", permissionType: "Modify", roleId: 12 },
36
- ],
37
30
  };
38
31
 
39
- const mockResponse: CreateField200Response = {
40
- id: 100,
41
- label: "TestField",
32
+ const mockResponse = {
33
+ id: 101,
34
+ label: "NewField",
42
35
  fieldType: "text",
43
- mode: "",
44
- noWrap: false,
45
- bold: false,
46
- required: false,
47
36
  appearsByDefault: true,
48
- findEnabled: true,
49
- unique: false,
50
- doesDataCopy: false,
51
- fieldHelp: "A test field",
52
37
  audited: false,
38
+ bold: false,
39
+ doesDataCopy: false,
40
+ fieldHelp: "",
41
+ findEnabled: true,
42
+ mode: "",
43
+ noWrap: false,
44
+ permissions: [],
53
45
  properties: {
54
- primaryKey: false,
55
- foreignKey: false,
56
- numLines: 1,
57
- maxLength: 0,
58
- appendOnly: false,
59
46
  allowHTML: false,
60
47
  allowMentions: false,
61
- sortAsGiven: false,
62
- carryChoices: true,
63
48
  allowNewChoices: false,
64
- formula: "",
49
+ appendOnly: false,
50
+ carryChoices: true,
65
51
  defaultValue: "",
52
+ foreignKey: false,
53
+ formula: "",
54
+ maxLength: 0,
55
+ numLines: 1,
56
+ primaryKey: false,
57
+ sortAsGiven: false,
66
58
  },
67
- permissions: [
68
- { permissionType: "View", role: "Viewer", roleId: 10 },
69
- { permissionType: "Modify", role: "Administrator", roleId: 12 },
70
- ],
59
+ required: false,
60
+ unique: false,
71
61
  };
72
62
 
73
63
  mockFetch.mockResolvedValueOnce({
@@ -82,25 +72,22 @@ describe("QuickbaseClient Unit - createField", () => {
82
72
  });
83
73
 
84
74
  expect(result).toEqual(mockResponse);
85
- const callArgs = mockFetch.mock.calls[0];
86
- console.log("Raw fetch call (user token):", callArgs); // Debug raw call
87
- const receivedBody = JSON.parse(callArgs[1].body as string);
88
- expect(receivedBody).toEqual(request); // Expect full body
89
75
  expect(mockFetch).toHaveBeenCalledWith(
90
76
  `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
91
- expect.objectContaining({
77
+ {
92
78
  method: "POST",
93
- headers: expect.objectContaining({
79
+ body: JSON.stringify(request),
80
+ headers: {
94
81
  "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
95
82
  Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
96
83
  "Content-Type": "application/json",
97
- }),
98
- body: JSON.stringify(request),
99
- })
84
+ },
85
+ credentials: "omit",
86
+ }
100
87
  );
101
88
  });
102
89
 
103
- it("creates a field successfully with temp token", async () => {
90
+ it("creates field successfully with temp token", async () => {
104
91
  client = createClient(mockFetch, { useTempTokens: true, debug: true });
105
92
 
106
93
  const request: CreateFieldRequest = {
@@ -109,35 +96,35 @@ describe("QuickbaseClient Unit - createField", () => {
109
96
  addToForms: false,
110
97
  };
111
98
 
112
- const mockResponse: CreateField200Response = {
113
- id: 101,
99
+ const mockResponse = {
100
+ id: 102,
114
101
  label: "TempField",
115
102
  fieldType: "numeric",
116
- mode: "",
117
- noWrap: false,
103
+ appearsByDefault: false,
104
+ audited: false,
118
105
  bold: false,
119
- required: false,
120
- appearsByDefault: true,
121
- findEnabled: true,
122
- unique: false,
123
106
  doesDataCopy: false,
124
107
  fieldHelp: "",
125
- audited: false,
108
+ findEnabled: true,
109
+ mode: "",
110
+ noWrap: false,
111
+ permissions: [],
126
112
  properties: {
127
- primaryKey: false,
128
- foreignKey: false,
129
- numLines: 1,
130
- maxLength: 0,
131
- appendOnly: false,
132
113
  allowHTML: false,
133
114
  allowMentions: false,
134
- sortAsGiven: false,
135
- carryChoices: true,
136
115
  allowNewChoices: false,
137
- formula: "",
116
+ appendOnly: false,
117
+ carryChoices: true,
138
118
  defaultValue: "",
119
+ foreignKey: false,
120
+ formula: "",
121
+ maxLength: 0,
122
+ numLines: 1,
123
+ primaryKey: false,
124
+ sortAsGiven: false,
139
125
  },
140
- permissions: [],
126
+ required: false,
127
+ unique: false,
141
128
  };
142
129
 
143
130
  mockFetch
@@ -162,29 +149,21 @@ describe("QuickbaseClient Unit - createField", () => {
162
149
  expect(mockFetch).toHaveBeenNthCalledWith(
163
150
  1,
164
151
  `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
165
- expect.objectContaining({
166
- method: "GET",
167
- headers: expect.objectContaining({
168
- "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
169
- "Content-Type": "application/json",
170
- }),
171
- credentials: "include",
172
- })
152
+ expect.any(Object)
173
153
  );
174
- const callArgs = mockFetch.mock.calls[1];
175
- console.log("Raw fetch call (temp token):", callArgs); // Debug raw call
176
154
  expect(mockFetch).toHaveBeenNthCalledWith(
177
155
  2,
178
156
  `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
179
- expect.objectContaining({
157
+ {
180
158
  method: "POST",
181
- headers: expect.objectContaining({
182
- "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
159
+ body: JSON.stringify(request),
160
+ headers: {
183
161
  Authorization: "QB-TEMP-TOKEN temp_token",
184
162
  "Content-Type": "application/json",
185
- }),
186
- body: JSON.stringify(request),
187
- })
163
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
164
+ },
165
+ credentials: "omit",
166
+ }
188
167
  );
189
168
  });
190
169
 
@@ -196,35 +175,35 @@ describe("QuickbaseClient Unit - createField", () => {
196
175
  fieldType: "checkbox",
197
176
  };
198
177
 
199
- const mockResponse: CreateField200Response = {
178
+ const mockResponse = {
200
179
  id: 102,
201
180
  label: "RetryField",
202
181
  fieldType: "checkbox",
203
- mode: "",
204
- noWrap: false,
205
- bold: false,
206
- required: false,
207
182
  appearsByDefault: true,
208
- findEnabled: true,
209
- unique: false,
183
+ audited: false,
184
+ bold: false,
210
185
  doesDataCopy: false,
211
186
  fieldHelp: "",
212
- audited: false,
187
+ findEnabled: true,
188
+ mode: "",
189
+ noWrap: false,
190
+ permissions: [],
213
191
  properties: {
214
- primaryKey: false,
215
- foreignKey: false,
216
- numLines: 1,
217
- maxLength: 0,
218
- appendOnly: false,
219
192
  allowHTML: false,
220
193
  allowMentions: false,
221
- sortAsGiven: false,
222
- carryChoices: true,
223
194
  allowNewChoices: false,
224
- formula: "",
195
+ appendOnly: false,
196
+ carryChoices: true,
225
197
  defaultValue: "",
198
+ foreignKey: false,
199
+ formula: "",
200
+ maxLength: 0,
201
+ numLines: 1,
202
+ primaryKey: false,
203
+ sortAsGiven: false,
226
204
  },
227
- permissions: [],
205
+ required: false,
206
+ unique: false,
228
207
  };
229
208
 
230
209
  mockFetch
@@ -238,6 +217,7 @@ describe("QuickbaseClient Unit - createField", () => {
238
217
  ok: false,
239
218
  status: 401,
240
219
  json: () => Promise.resolve({ message: "Unauthorized" }),
220
+ text: () => Promise.resolve("Unauthorized"),
241
221
  })
242
222
  .mockResolvedValueOnce({
243
223
  ok: true,
@@ -266,11 +246,16 @@ describe("QuickbaseClient Unit - createField", () => {
266
246
  expect(mockFetch).toHaveBeenNthCalledWith(
267
247
  2,
268
248
  `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
269
- expect.objectContaining({
270
- headers: expect.objectContaining({
249
+ {
250
+ method: "POST",
251
+ body: JSON.stringify(request),
252
+ headers: {
271
253
  Authorization: "QB-TEMP-TOKEN initial_token",
272
- }),
273
- })
254
+ "Content-Type": "application/json",
255
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
256
+ },
257
+ credentials: "omit",
258
+ }
274
259
  );
275
260
  expect(mockFetch).toHaveBeenNthCalledWith(
276
261
  3,
@@ -280,18 +265,22 @@ describe("QuickbaseClient Unit - createField", () => {
280
265
  expect(mockFetch).toHaveBeenNthCalledWith(
281
266
  4,
282
267
  `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
283
- expect.objectContaining({
284
- headers: expect.objectContaining({
268
+ {
269
+ method: "POST",
270
+ body: JSON.stringify(request),
271
+ headers: {
285
272
  Authorization: "QB-TEMP-TOKEN new_token",
286
- }),
287
- })
273
+ "Content-Type": "application/json",
274
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
275
+ },
276
+ credentials: "omit",
277
+ }
288
278
  );
289
279
  expect(consoleSpy).toHaveBeenCalledWith(
290
- "Authorization error for createField, refreshing token:",
291
- expect.any(String)
280
+ "Authorization error for createField (temp token), refreshing token:"
292
281
  );
293
282
  expect(consoleSpy).toHaveBeenCalledWith(
294
- "Retrying createField with new token"
283
+ "[createField] Retrying with token: new_token..."
295
284
  );
296
285
  consoleSpy.mockRestore();
297
286
  });
@@ -300,7 +289,7 @@ describe("QuickbaseClient Unit - createField", () => {
300
289
  client = createClient(mockFetch, { debug: true });
301
290
 
302
291
  const request: CreateFieldRequest = {
303
- label: "", // Invalid: label is required
292
+ label: "", // Invalid: empty label
304
293
  fieldType: "text",
305
294
  };
306
295
 
@@ -308,6 +297,7 @@ describe("QuickbaseClient Unit - createField", () => {
308
297
  ok: false,
309
298
  status: 400,
310
299
  json: () => Promise.resolve({ message: "Label is required" }),
300
+ text: () => Promise.resolve("Label is required"),
311
301
  });
312
302
 
313
303
  await expect(
@@ -316,15 +306,16 @@ describe("QuickbaseClient Unit - createField", () => {
316
306
 
317
307
  expect(mockFetch).toHaveBeenCalledWith(
318
308
  `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
319
- expect.objectContaining({
309
+ {
320
310
  method: "POST",
321
- headers: expect.objectContaining({
311
+ body: JSON.stringify(request),
312
+ headers: {
322
313
  "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
323
314
  Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
324
315
  "Content-Type": "application/json",
325
- }),
326
- body: JSON.stringify(request),
327
- })
316
+ },
317
+ credentials: "omit",
318
+ }
328
319
  );
329
320
  });
330
321
 
@@ -347,22 +338,49 @@ describe("QuickbaseClient Unit - createField", () => {
347
338
  ok: false,
348
339
  status: 401,
349
340
  json: () => Promise.resolve({ message: "Unauthorized" }),
341
+ text: () => Promise.resolve("Unauthorized"),
342
+ })
343
+ .mockResolvedValueOnce({
344
+ ok: true,
345
+ status: 200,
346
+ json: () => Promise.resolve({ temporaryAuthorization: "token_2" }),
350
347
  })
351
348
  .mockResolvedValueOnce({
352
349
  ok: false,
353
350
  status: 401,
354
- json: () =>
355
- Promise.resolve({ message: "Unauthorized in fetchTempToken" }),
351
+ json: () => Promise.resolve({ message: "Unauthorized again" }),
352
+ text: () => Promise.resolve("Unauthorized again"),
353
+ })
354
+ .mockResolvedValueOnce({
355
+ ok: true,
356
+ status: 200,
357
+ json: () => Promise.resolve({ temporaryAuthorization: "token_3" }),
358
+ })
359
+ .mockResolvedValueOnce({
360
+ ok: false,
361
+ status: 401,
362
+ json: () => Promise.resolve({ message: "Unauthorized again" }),
363
+ text: () => Promise.resolve("Unauthorized again"),
364
+ })
365
+ .mockResolvedValueOnce({
366
+ ok: true,
367
+ status: 200,
368
+ json: () => Promise.resolve({ temporaryAuthorization: "token_4" }),
369
+ })
370
+ .mockResolvedValueOnce({
371
+ ok: false,
372
+ status: 401,
373
+ json: () => Promise.resolve({ message: "Unauthorized again" }),
374
+ text: () => Promise.resolve("Unauthorized again"),
356
375
  });
357
376
 
358
377
  const consoleSpy = vi.spyOn(console, "log");
378
+
359
379
  await expect(
360
380
  client.createField({ tableId: QB_TABLE_ID_1, body: request })
361
- ).rejects.toThrow(
362
- "API Error: Unauthorized in fetchTempToken (Status: 401)"
363
- );
381
+ ).rejects.toThrow("API Error: Unauthorized again (Status: 401)");
364
382
 
365
- expect(mockFetch).toHaveBeenCalledTimes(3);
383
+ expect(mockFetch).toHaveBeenCalledTimes(8);
366
384
  expect(mockFetch).toHaveBeenNthCalledWith(
367
385
  1,
368
386
  `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
@@ -372,6 +390,7 @@ describe("QuickbaseClient Unit - createField", () => {
372
390
  2,
373
391
  `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
374
392
  expect.objectContaining({
393
+ method: "POST",
375
394
  headers: expect.objectContaining({
376
395
  Authorization: "QB-TEMP-TOKEN initial_token",
377
396
  }),
@@ -382,10 +401,51 @@ describe("QuickbaseClient Unit - createField", () => {
382
401
  `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
383
402
  expect.any(Object)
384
403
  );
404
+ expect(mockFetch).toHaveBeenNthCalledWith(
405
+ 4,
406
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
407
+ expect.objectContaining({
408
+ method: "POST",
409
+ headers: expect.objectContaining({
410
+ Authorization: "QB-TEMP-TOKEN token_2",
411
+ }),
412
+ })
413
+ );
414
+ expect(mockFetch).toHaveBeenNthCalledWith(
415
+ 5,
416
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
417
+ expect.any(Object)
418
+ );
419
+ expect(mockFetch).toHaveBeenNthCalledWith(
420
+ 6,
421
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
422
+ expect.objectContaining({
423
+ method: "POST",
424
+ headers: expect.objectContaining({
425
+ Authorization: "QB-TEMP-TOKEN token_3",
426
+ }),
427
+ })
428
+ );
429
+ expect(mockFetch).toHaveBeenNthCalledWith(
430
+ 7,
431
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
432
+ expect.any(Object)
433
+ );
434
+ expect(mockFetch).toHaveBeenNthCalledWith(
435
+ 8,
436
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
437
+ expect.objectContaining({
438
+ method: "POST",
439
+ headers: expect.objectContaining({
440
+ Authorization: "QB-TEMP-TOKEN token_4",
441
+ }),
442
+ })
443
+ );
444
+
385
445
  expect(consoleSpy).toHaveBeenCalledWith(
386
- "Authorization error for createField, refreshing token:",
387
- expect.any(String)
446
+ "Authorization error for createField (temp token), refreshing token:"
388
447
  );
448
+
389
449
  consoleSpy.mockRestore();
390
450
  });
391
451
  });
@@ -1,4 +1,3 @@
1
- // tests/vitest/unit/fields/getFieldUsage.test.ts
2
1
  import { describe, it, expect, beforeEach } from "vitest";
3
2
  import {
4
3
  createClient,
@@ -27,7 +26,7 @@ describe("QuickbaseClient Unit - getFieldUsage", () => {
27
26
  field: {
28
27
  id: 6,
29
28
  name: "name",
30
- type: "text", // Match QuickBase's lowercase response
29
+ type: "text",
31
30
  },
32
31
  usage: {
33
32
  actions: { count: 0 },
@@ -50,7 +49,6 @@ describe("QuickbaseClient Unit - getFieldUsage", () => {
50
49
  },
51
50
  };
52
51
 
53
- // Mock the response as an array to match the updated API behavior
54
52
  mockFetch.mockResolvedValueOnce(
55
53
  new Response(JSON.stringify([mockResponse]), {
56
54
  status: 200,
@@ -64,7 +62,6 @@ describe("QuickbaseClient Unit - getFieldUsage", () => {
64
62
  tableId: QB_TABLE_ID_1,
65
63
  });
66
64
 
67
- // Expect an array and unwrap the first item
68
65
  expect(result).toBeInstanceOf(Array);
69
66
  expect(result).toHaveLength(1);
70
67
  const fieldUsage = result[0];
@@ -115,14 +112,37 @@ describe("QuickbaseClient Unit - getFieldUsage", () => {
115
112
  );
116
113
  });
117
114
 
118
- it("handles 401 error and does not retry with user token", async () => {
119
- mockFetch.mockResolvedValueOnce(
120
- new Response(JSON.stringify({ message: "Unauthorized" }), {
121
- status: 401,
122
- statusText: "Unauthorized",
123
- headers: { "Content-Type": "application/json" },
124
- })
125
- );
115
+ it("handles persistent 401 error for field usage retrieval", async () => {
116
+ // Simulate persistent 401 errors to exhaust retries
117
+ mockFetch
118
+ .mockResolvedValueOnce(
119
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
120
+ status: 401,
121
+ statusText: "Unauthorized",
122
+ headers: { "Content-Type": "application/json" },
123
+ })
124
+ )
125
+ .mockResolvedValueOnce(
126
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
127
+ status: 401,
128
+ statusText: "Unauthorized",
129
+ headers: { "Content-Type": "application/json" },
130
+ })
131
+ )
132
+ .mockResolvedValueOnce(
133
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
134
+ status: 401,
135
+ statusText: "Unauthorized",
136
+ headers: { "Content-Type": "application/json" },
137
+ })
138
+ )
139
+ .mockResolvedValueOnce(
140
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
141
+ status: 401,
142
+ statusText: "Unauthorized",
143
+ headers: { "Content-Type": "application/json" },
144
+ })
145
+ );
126
146
 
127
147
  await expect(
128
148
  client.getFieldUsage({
@@ -131,8 +151,21 @@ describe("QuickbaseClient Unit - getFieldUsage", () => {
131
151
  })
132
152
  ).rejects.toThrow("API Error: Unauthorized (Status: 401)");
133
153
 
134
- expect(mockFetch).toHaveBeenCalledTimes(1); // No retry with user token
135
- expect(mockFetch).toHaveBeenCalledWith(
154
+ expect(mockFetch).toHaveBeenCalledTimes(4); // Initial + 3 retries
155
+ expect(mockFetch).toHaveBeenNthCalledWith(
156
+ 1,
157
+ `https://api.quickbase.com/v1/fields/usage/6?tableId=${QB_TABLE_ID_1}`,
158
+ expect.objectContaining({
159
+ method: "GET",
160
+ headers: expect.objectContaining({
161
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
162
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
163
+ "Content-Type": "application/json",
164
+ }),
165
+ })
166
+ );
167
+ expect(mockFetch).toHaveBeenNthCalledWith(
168
+ 4,
136
169
  `https://api.quickbase.com/v1/fields/usage/6?tableId=${QB_TABLE_ID_1}`,
137
170
  expect.objectContaining({
138
171
  method: "GET",