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

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 (60) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/esm/quickbase.d.ts +589 -125
  3. package/dist/esm/quickbase.js +484 -145
  4. package/dist/esm/quickbase.js.map +1 -1
  5. package/dist/esm/quickbase.min.js +1 -1
  6. package/dist/esm/quickbase.min.js.map +1 -1
  7. package/dist/umd/quickbase.umd.js +492 -144
  8. package/dist/umd/quickbase.umd.js.map +1 -1
  9. package/dist/umd/quickbase.umd.min.js +1 -1
  10. package/dist/umd/quickbase.umd.min.js.map +1 -1
  11. package/open-api/fix-spec-main.ts +69 -68
  12. package/open-api/generate-override-report.ts +214 -0
  13. package/open-api/generate-unified-interface.ts +73 -94
  14. package/open-api/output/override-report.html +11 -0
  15. package/open-api/output/override-report.json +14395 -58
  16. package/open-api/output/quickbase-fixed.json +583 -2
  17. package/open-api/schema/tags/fields.ts +331 -16
  18. package/open-api/schema/tags/index.ts +2 -4
  19. package/open-api/types/spec.ts +2 -1
  20. package/package.json +31 -33
  21. package/src/RateLimitError.ts +12 -0
  22. package/src/ThrottleBucket.ts +36 -0
  23. package/src/generated-unified/QuickbaseClient.ts +119 -119
  24. package/src/invokeMethod.ts +357 -0
  25. package/src/quickbaseClient.ts +51 -209
  26. package/tests/setup.ts +2 -0
  27. package/tests/vitest/qb/apps/getApp.test.ts +6 -2
  28. package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
  29. package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
  30. package/tests/vitest/qb/auth/retryUserToken401.test.ts +56 -0
  31. package/tests/vitest/qb/fields/createField.test.ts +1 -2
  32. package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
  33. package/tests/vitest/qb/fields/updateField.test.ts +93 -0
  34. package/tests/vitest/qb/index.ts +1 -0
  35. package/tests/vitest/qb/tables/updateRelationship.test.ts +277 -0
  36. package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
  37. package/tests/vitest/unit/apps/updateApp.test.ts +381 -0
  38. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +6 -26
  39. package/tests/vitest/unit/auth/{retryOn401.test.ts → retryTempToken401.test.ts} +3 -6
  40. package/tests/vitest/unit/auth/{two401s.test.ts → retryTempTokenTwo401.test.ts} +4 -9
  41. package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -0
  42. package/tests/vitest/unit/fields/createField.test.ts +8 -12
  43. package/tests/vitest/unit/fields/getFieldUsage.test.ts +33 -14
  44. package/tests/vitest/unit/fields/updateField.test.ts +280 -0
  45. package/tests/vitest/unit/rateLimit.test.ts +189 -0
  46. package/tests/vitest/unit/tables/updateRelationship.test.ts +305 -0
  47. package/tests/vitest/unit/tokenLifespan.test.ts +138 -0
  48. package/tools/log-gen.ts +8 -4
  49. package/tools/log-gen.yaml +3004 -1848
  50. package/tsconfig.json +5 -2
  51. package/vitest.config.ts +2 -2
  52. package/notes.txt +0 -158
  53. package/tools/generate-types.ts +0 -29
  54. package/tools/openapi-schema-validator.ts +0 -38
  55. package/tools/spec-analyzer.ts +0 -163
  56. package/tools/split-spec.ts +0 -148
  57. package/tools/test-import.ts +0 -3
  58. package/tools/validate-spec.ts +0 -40
  59. /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
  60. /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
@@ -0,0 +1,189 @@
1
+ // tests/vitest/unit/rateLimit.test.ts
2
+ import { describe, it, expect, beforeEach } from "vitest";
3
+ import {
4
+ createClient,
5
+ mockFetch,
6
+ QB_APP_ID,
7
+ QB_REALM,
8
+ QB_USER_TOKEN,
9
+ } from "@tests/setup.ts";
10
+ import { RateLimitError } from "@/RateLimitError";
11
+ import { GetApp200Response } from "@/generated/models";
12
+
13
+ describe("QuickbaseClient Unit - Rate Limit Handling", () => {
14
+ let client: ReturnType<typeof createClient>;
15
+
16
+ beforeEach(() => {
17
+ mockFetch.mockClear();
18
+ });
19
+
20
+ it("succeeds after retrying on 429 Too Many Requests", async () => {
21
+ client = createClient(mockFetch, {
22
+ realm: QB_REALM,
23
+ userToken: QB_USER_TOKEN,
24
+ debug: true,
25
+ maxRetries: 2,
26
+ retryDelay: 100,
27
+ throttle: { rate: 10, burst: 10 },
28
+ });
29
+
30
+ const appId = QB_APP_ID;
31
+
32
+ const mockResponse: GetApp200Response = {
33
+ id: appId,
34
+ name: "TestApp",
35
+ created: "2025-01-01T00:00:00Z",
36
+ updated: "2025-01-02T00:00:00Z",
37
+ description: "Test app",
38
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
39
+ dateFormat: "MM-DD-YYYY",
40
+ hasEveryoneOnTheInternet: false,
41
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
42
+ securityProperties: {
43
+ allowClone: false,
44
+ allowExport: true,
45
+ enableAppTokens: true,
46
+ hideFromPublic: false,
47
+ mustBeRealmApproved: false,
48
+ useIPFilter: false,
49
+ },
50
+ };
51
+
52
+ mockFetch
53
+ .mockResolvedValueOnce({
54
+ ok: false,
55
+ status: 429,
56
+ headers: new Headers({ "Retry-After": "1" }),
57
+ json: () => Promise.resolve({ message: "Too Many Requests" }),
58
+ })
59
+ .mockResolvedValueOnce({
60
+ ok: true,
61
+ status: 200,
62
+ json: () => Promise.resolve(mockResponse),
63
+ });
64
+
65
+ const result = await client.getApp({ appId });
66
+
67
+ expect(result).toEqual({
68
+ ...mockResponse,
69
+ created: new Date("2025-01-01T00:00:00Z"),
70
+ updated: new Date("2025-01-02T00:00:00Z"),
71
+ });
72
+ expect(mockFetch).toHaveBeenCalledTimes(2);
73
+ expect(mockFetch).toHaveBeenNthCalledWith(
74
+ 1,
75
+ `https://api.quickbase.com/v1/apps/${appId}`,
76
+ expect.objectContaining({
77
+ method: "GET",
78
+ headers: expect.objectContaining({
79
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
80
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
81
+ "Content-Type": "application/json",
82
+ }),
83
+ credentials: "omit",
84
+ })
85
+ );
86
+ expect(mockFetch).toHaveBeenNthCalledWith(
87
+ 2,
88
+ `https://api.quickbase.com/v1/apps/${appId}`,
89
+ expect.objectContaining({
90
+ method: "GET",
91
+ headers: expect.objectContaining({
92
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
93
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
94
+ "Content-Type": "application/json",
95
+ }),
96
+ credentials: "omit",
97
+ })
98
+ );
99
+ });
100
+
101
+ it("throws RateLimitError after max retries on 429", async () => {
102
+ client = createClient(mockFetch, {
103
+ realm: QB_REALM,
104
+ userToken: QB_USER_TOKEN,
105
+ debug: true,
106
+ maxRetries: 1,
107
+ retryDelay: 100,
108
+ throttle: { rate: 10, burst: 10 },
109
+ });
110
+
111
+ const appId = QB_APP_ID;
112
+
113
+ mockFetch
114
+ .mockResolvedValueOnce({
115
+ ok: false,
116
+ status: 429,
117
+ headers: new Headers({ "Retry-After": "1" }),
118
+ json: () => Promise.resolve({ message: "Too Many Requests" }),
119
+ })
120
+ .mockResolvedValueOnce({
121
+ ok: false,
122
+ status: 429,
123
+ headers: new Headers({ "Retry-After": "1" }),
124
+ json: () => Promise.resolve({ message: "Too Many Requests" }),
125
+ });
126
+
127
+ await expect(client.getApp({ appId })).rejects.toMatchObject({
128
+ name: "RateLimitError",
129
+ message: "API Error: Too Many Requests (Status: 429)",
130
+ status: 429,
131
+ retryAfter: 1,
132
+ });
133
+
134
+ expect(mockFetch).toHaveBeenCalledTimes(2);
135
+ });
136
+
137
+ it("respects throttle limits without exceeding rate", async () => {
138
+ client = createClient(mockFetch, {
139
+ realm: QB_REALM,
140
+ userToken: QB_USER_TOKEN,
141
+ debug: true,
142
+ throttle: { rate: 5, burst: 5 },
143
+ });
144
+
145
+ const appId = QB_APP_ID;
146
+
147
+ const mockResponse: GetApp200Response = {
148
+ id: appId,
149
+ name: "TestApp",
150
+ created: "2025-01-01T00:00:00Z",
151
+ updated: "2025-01-02T00:00:00Z",
152
+ description: "Test app",
153
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
154
+ dateFormat: "MM-DD-YYYY",
155
+ hasEveryoneOnTheInternet: false,
156
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
157
+ securityProperties: {
158
+ allowClone: false,
159
+ allowExport: true,
160
+ enableAppTokens: true,
161
+ hideFromPublic: false,
162
+ mustBeRealmApproved: false,
163
+ useIPFilter: false,
164
+ },
165
+ };
166
+
167
+ mockFetch.mockResolvedValue({
168
+ ok: true,
169
+ status: 200,
170
+ json: () => Promise.resolve(mockResponse),
171
+ });
172
+
173
+ const startTime = Date.now();
174
+ await Promise.all([
175
+ client.getApp({ appId }),
176
+ client.getApp({ appId }),
177
+ client.getApp({ appId }),
178
+ client.getApp({ appId }),
179
+ client.getApp({ appId }),
180
+ client.getApp({ appId }), // 6th call triggers throttle
181
+ ]);
182
+ const endTime = Date.now();
183
+ const durationMs = endTime - startTime;
184
+
185
+ expect(mockFetch).toHaveBeenCalledTimes(6);
186
+ expect(durationMs).toBeGreaterThanOrEqual(200);
187
+ expect(durationMs).toBeLessThan(1000);
188
+ });
189
+ });
@@ -0,0 +1,305 @@
1
+ // tests/vitest/unit/tables/updateRelationship.test.ts
2
+
3
+ import { describe, it, expect, beforeEach, vi } from "vitest";
4
+ import {
5
+ createClient,
6
+ mockFetch,
7
+ QB_REALM,
8
+ QB_USER_TOKEN,
9
+ QB_TABLE_ID_1,
10
+ } from "@tests/setup.ts";
11
+ import {
12
+ UpdateRelationshipRequest,
13
+ UpdateRelationship200Response,
14
+ } from "@/generated/models";
15
+
16
+ describe("QuickbaseClient Unit - updateRelationship", () => {
17
+ let client: ReturnType<typeof createClient>;
18
+
19
+ beforeEach(() => {
20
+ mockFetch.mockClear();
21
+ });
22
+
23
+ it("initializes without errors and has updateRelationship method", () => {
24
+ client = createClient(mockFetch, { debug: true });
25
+ expect(client).toBeDefined();
26
+ expect(typeof client.updateRelationship).toBe("function");
27
+ });
28
+
29
+ it("updates relationship successfully with user token", async () => {
30
+ client = createClient(mockFetch, { debug: true });
31
+
32
+ const tableId = QB_TABLE_ID_1;
33
+ const relationshipId = 6;
34
+
35
+ const request: UpdateRelationshipRequest = {
36
+ lookupFieldIds: [4, 5, 6],
37
+ summaryFields: [
38
+ {
39
+ summaryFid: 3,
40
+ label: "my summary field",
41
+ accumulationType: "COUNT",
42
+ where: "{'3'.EX.'1'}",
43
+ },
44
+ ],
45
+ };
46
+
47
+ const mockResponse: UpdateRelationship200Response = {
48
+ id: relationshipId,
49
+ parentTableId: "bck7gp3q2",
50
+ childTableId: tableId,
51
+ foreignKeyField: {
52
+ id: relationshipId,
53
+ label: "Related record",
54
+ type: "numeric",
55
+ },
56
+ isCrossApp: false,
57
+ lookupFields: [
58
+ { id: 4, label: "Field 4", type: "text" },
59
+ { id: 5, label: "Field 5", type: "numeric" },
60
+ { id: 6, label: "Field 6", type: "date" },
61
+ ],
62
+ summaryFields: [{ id: 8, label: "my summary field", type: "numeric" }],
63
+ };
64
+
65
+ mockFetch.mockResolvedValueOnce({
66
+ ok: true,
67
+ status: 200,
68
+ json: () => Promise.resolve(mockResponse),
69
+ });
70
+
71
+ const result = await client.updateRelationship({
72
+ tableId,
73
+ relationshipId,
74
+ body: request,
75
+ });
76
+
77
+ expect(result).toEqual(mockResponse);
78
+ expect(mockFetch).toHaveBeenCalledWith(
79
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
80
+ expect.objectContaining({
81
+ method: "POST",
82
+ headers: expect.objectContaining({
83
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
84
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
85
+ "Content-Type": "application/json",
86
+ }),
87
+ body: JSON.stringify(request),
88
+ credentials: "omit",
89
+ })
90
+ );
91
+ });
92
+
93
+ it("updates relationship successfully with temp token", async () => {
94
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
95
+
96
+ const tableId = QB_TABLE_ID_1;
97
+ const relationshipId = 6;
98
+
99
+ const request: UpdateRelationshipRequest = {
100
+ lookupFieldIds: [7],
101
+ summaryFields: [
102
+ {
103
+ summaryFid: 0,
104
+ label: "Count of Records",
105
+ accumulationType: "COUNT",
106
+ where: "{'3'.GT.0}",
107
+ },
108
+ ],
109
+ };
110
+
111
+ const mockResponse: UpdateRelationship200Response = {
112
+ id: relationshipId,
113
+ parentTableId: "bck7gp3q2",
114
+ childTableId: tableId,
115
+ foreignKeyField: {
116
+ id: relationshipId,
117
+ label: "Related record",
118
+ type: "numeric",
119
+ },
120
+ isCrossApp: false,
121
+ lookupFields: [{ id: 7, label: "record - text field", type: "text" }],
122
+ summaryFields: [{ id: 9, label: "Count of Records", type: "numeric" }],
123
+ };
124
+
125
+ mockFetch
126
+ .mockResolvedValueOnce({
127
+ ok: true,
128
+ status: 200,
129
+ json: () => Promise.resolve({ temporaryAuthorization: "temp_token" }),
130
+ })
131
+ .mockResolvedValueOnce({
132
+ ok: true,
133
+ status: 200,
134
+ json: () => Promise.resolve(mockResponse),
135
+ });
136
+
137
+ const result = await client.updateRelationship({
138
+ tableId,
139
+ relationshipId,
140
+ body: request,
141
+ });
142
+
143
+ expect(result).toEqual(mockResponse);
144
+ expect(mockFetch).toHaveBeenCalledTimes(2);
145
+ expect(mockFetch).toHaveBeenNthCalledWith(
146
+ 1,
147
+ `https://api.quickbase.com/v1/auth/temporary/${tableId}`,
148
+ expect.objectContaining({
149
+ method: "GET",
150
+ headers: expect.objectContaining({
151
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
152
+ "Content-Type": "application/json",
153
+ }),
154
+ credentials: "include",
155
+ })
156
+ );
157
+ expect(mockFetch).toHaveBeenNthCalledWith(
158
+ 2,
159
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
160
+ expect.objectContaining({
161
+ method: "POST",
162
+ headers: expect.objectContaining({
163
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
164
+ Authorization: "QB-TEMP-TOKEN temp_token",
165
+ "Content-Type": "application/json",
166
+ }),
167
+ body: JSON.stringify(request),
168
+ credentials: "omit",
169
+ })
170
+ );
171
+ });
172
+
173
+ it("retries successfully after 401 with temp token", async () => {
174
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
175
+
176
+ const tableId = QB_TABLE_ID_1;
177
+ const relationshipId = 6;
178
+
179
+ const request: UpdateRelationshipRequest = {
180
+ lookupFieldIds: [4],
181
+ };
182
+
183
+ const mockResponse: UpdateRelationship200Response = {
184
+ id: relationshipId,
185
+ parentTableId: "bck7gp3q2",
186
+ childTableId: tableId,
187
+ foreignKeyField: {
188
+ id: relationshipId,
189
+ label: "Related record",
190
+ type: "numeric",
191
+ },
192
+ isCrossApp: false,
193
+ lookupFields: [{ id: 4, label: "Field 4", type: "text" }],
194
+ summaryFields: [],
195
+ };
196
+
197
+ mockFetch
198
+ .mockResolvedValueOnce({
199
+ ok: true,
200
+ status: 200,
201
+ json: () =>
202
+ Promise.resolve({ temporaryAuthorization: "initial_token" }),
203
+ })
204
+ .mockResolvedValueOnce({
205
+ ok: false,
206
+ status: 401,
207
+ json: () => Promise.resolve({ message: "Unauthorized" }),
208
+ })
209
+ .mockResolvedValueOnce({
210
+ ok: true,
211
+ status: 200,
212
+ json: () => Promise.resolve({ temporaryAuthorization: "new_token" }),
213
+ })
214
+ .mockResolvedValueOnce({
215
+ ok: true,
216
+ status: 200,
217
+ json: () => Promise.resolve(mockResponse),
218
+ });
219
+
220
+ const consoleSpy = vi.spyOn(console, "log");
221
+ const result = await client.updateRelationship({
222
+ tableId,
223
+ relationshipId,
224
+ body: request,
225
+ });
226
+
227
+ expect(result).toEqual(mockResponse);
228
+ expect(mockFetch).toHaveBeenCalledTimes(4);
229
+ expect(mockFetch).toHaveBeenNthCalledWith(
230
+ 1,
231
+ `https://api.quickbase.com/v1/auth/temporary/${tableId}`,
232
+ expect.any(Object)
233
+ );
234
+ expect(mockFetch).toHaveBeenNthCalledWith(
235
+ 2,
236
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
237
+ expect.objectContaining({
238
+ headers: expect.objectContaining({
239
+ Authorization: "QB-TEMP-TOKEN initial_token",
240
+ }),
241
+ })
242
+ );
243
+ expect(mockFetch).toHaveBeenNthCalledWith(
244
+ 3,
245
+ `https://api.quickbase.com/v1/auth/temporary/${tableId}`,
246
+ expect.any(Object)
247
+ );
248
+ expect(mockFetch).toHaveBeenNthCalledWith(
249
+ 4,
250
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
251
+ expect.objectContaining({
252
+ headers: expect.objectContaining({
253
+ Authorization: "QB-TEMP-TOKEN new_token",
254
+ }),
255
+ })
256
+ );
257
+ expect(consoleSpy).toHaveBeenCalledWith(
258
+ "Authorization error for updateRelationship (temp token), refreshing token:",
259
+ expect.any(String)
260
+ );
261
+ expect(consoleSpy).toHaveBeenCalledWith(
262
+ "Retrying updateRelationship with temp token"
263
+ );
264
+ consoleSpy.mockRestore();
265
+ });
266
+
267
+ it("handles 400 error for invalid request", async () => {
268
+ client = createClient(mockFetch, { debug: true });
269
+
270
+ const tableId = QB_TABLE_ID_1;
271
+ const relationshipId = 6;
272
+
273
+ const request: UpdateRelationshipRequest = {
274
+ lookupFieldIds: [-1],
275
+ };
276
+
277
+ mockFetch.mockResolvedValueOnce({
278
+ ok: false,
279
+ status: 400,
280
+ json: () => Promise.resolve({ message: "Invalid field ID" }),
281
+ });
282
+
283
+ await expect(
284
+ client.updateRelationship({
285
+ tableId,
286
+ relationshipId,
287
+ body: request,
288
+ })
289
+ ).rejects.toThrow("API Error: Invalid field ID (Status: 400)");
290
+
291
+ expect(mockFetch).toHaveBeenCalledWith(
292
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
293
+ expect.objectContaining({
294
+ method: "POST",
295
+ headers: expect.objectContaining({
296
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
297
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
298
+ "Content-Type": "application/json",
299
+ }),
300
+ body: JSON.stringify(request),
301
+ credentials: "omit",
302
+ })
303
+ );
304
+ });
305
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { createClient, mockFetch, QB_APP_ID, QB_REALM } from "@tests/setup.ts";
3
+ import { UpdateAppRequest, UpdateApp200Response } from "@/generated/models";
4
+
5
+ describe("QuickbaseClient Unit - Token Lifespan", () => {
6
+ let client: ReturnType<typeof createClient>;
7
+
8
+ beforeEach(() => {
9
+ mockFetch.mockClear();
10
+ });
11
+
12
+ it("reuses token within lifespan and fetches new token after expiration", async () => {
13
+ // Set a short lifespan of 500ms for testing
14
+ client = createClient(mockFetch, {
15
+ realm: QB_REALM,
16
+ useTempTokens: true,
17
+ tokenLifespan: 500, // 0.5 seconds
18
+ debug: true,
19
+ });
20
+
21
+ const request: UpdateAppRequest = {
22
+ name: "Test App",
23
+ };
24
+ const mockResponse: UpdateApp200Response = {
25
+ id: QB_APP_ID,
26
+ name: "Test App",
27
+ description: "",
28
+ created: new Date("2020-01-01T00:00:00Z"),
29
+ updated: new Date("2020-01-01T00:00:00Z"),
30
+ dateFormat: "MM-DD-YYYY",
31
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
32
+ hasEveryoneOnTheInternet: false,
33
+ ancestorId: null,
34
+ variables: [],
35
+ securityProperties: {},
36
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
37
+ dataClassification: "None",
38
+ };
39
+
40
+ // First call: Fetch a new token
41
+ mockFetch
42
+ .mockResolvedValueOnce({
43
+ ok: true,
44
+ status: 200,
45
+ json: () => Promise.resolve({ temporaryAuthorization: "token1" }),
46
+ })
47
+ .mockResolvedValueOnce({
48
+ ok: true,
49
+ status: 200,
50
+ json: () => Promise.resolve(mockResponse),
51
+ });
52
+
53
+ await client.updateApp({ appId: QB_APP_ID, body: request });
54
+ expect(mockFetch).toHaveBeenCalledTimes(2); // Token fetch + API call
55
+
56
+ // Second call within lifespan: Should reuse token1
57
+ mockFetch.mockResolvedValueOnce({
58
+ ok: true,
59
+ status: 200,
60
+ json: () => Promise.resolve(mockResponse),
61
+ });
62
+ await client.updateApp({ appId: QB_APP_ID, body: request });
63
+ expect(mockFetch).toHaveBeenCalledTimes(3); // Only API call, no new token fetch
64
+
65
+ // Wait for token to expire (500ms + buffer)
66
+ await new Promise((resolve) => setTimeout(resolve, 600));
67
+
68
+ // Third call after expiration: Should fetch a new token
69
+ mockFetch
70
+ .mockResolvedValueOnce({
71
+ ok: true,
72
+ status: 200,
73
+ json: () => Promise.resolve({ temporaryAuthorization: "token2" }),
74
+ })
75
+ .mockResolvedValueOnce({
76
+ ok: true,
77
+ status: 200,
78
+ json: () => Promise.resolve(mockResponse),
79
+ });
80
+ await client.updateApp({ appId: QB_APP_ID, body: request });
81
+ expect(mockFetch).toHaveBeenCalledTimes(5); // New token fetch + API call
82
+ });
83
+
84
+ it("uses default lifespan when not specified", async () => {
85
+ // No tokenLifespan provided, should use default (4:50 = 290,000ms)
86
+ client = createClient(mockFetch, {
87
+ realm: QB_REALM,
88
+ useTempTokens: true,
89
+ debug: true,
90
+ });
91
+
92
+ const request: UpdateAppRequest = {
93
+ name: "Default Lifespan Test",
94
+ };
95
+ const mockResponse: UpdateApp200Response = {
96
+ id: QB_APP_ID,
97
+ name: "Default Lifespan Test",
98
+ description: "",
99
+ created: new Date("2020-01-01T00:00:00Z"),
100
+ updated: new Date("2020-01-01T00:00:00Z"),
101
+ dateFormat: "MM-DD-YYYY",
102
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
103
+ hasEveryoneOnTheInternet: false,
104
+ ancestorId: null,
105
+ variables: [],
106
+ securityProperties: {},
107
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
108
+ dataClassification: "None",
109
+ };
110
+
111
+ // First call: Fetch a new token
112
+ mockFetch
113
+ .mockResolvedValueOnce({
114
+ ok: true,
115
+ status: 200,
116
+ json: () =>
117
+ Promise.resolve({ temporaryAuthorization: "default_token" }),
118
+ })
119
+ .mockResolvedValueOnce({
120
+ ok: true,
121
+ status: 200,
122
+ json: () => Promise.resolve(mockResponse),
123
+ });
124
+
125
+ await client.updateApp({ appId: QB_APP_ID, body: request });
126
+ expect(mockFetch).toHaveBeenCalledTimes(2); // Token fetch + API call
127
+
128
+ // Second call within default lifespan (e.g., after 1 second, well below 4:50)
129
+ mockFetch.mockResolvedValueOnce({
130
+ ok: true,
131
+ status: 200,
132
+ json: () => Promise.resolve(mockResponse),
133
+ });
134
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
135
+ await client.updateApp({ appId: QB_APP_ID, body: request });
136
+ expect(mockFetch).toHaveBeenCalledTimes(3); // No new token fetch, still within 4:50
137
+ });
138
+ });
package/tools/log-gen.ts CHANGED
@@ -18,20 +18,20 @@ const excludeDirs: string[] = [
18
18
  // prettier-ignore
19
19
  const includeFolders: string[] = [
20
20
  // "open-api",
21
- // "open-api/definitions",
22
- // "open-api/paths",
23
21
  ];
24
22
 
25
23
  // prettier-ignore
26
24
  const includeRecursiveFolders: string[] = [
27
- "open-api"
25
+ // "open-api"
28
26
  // "tests",
29
27
  // "tests/vitest/unit",
30
- // "tests/vitest/qb"
28
+ "tests/vitest/qb"
31
29
  ];
32
30
 
33
31
  // prettier-ignore
34
32
  const includeFiles: string[] = [
33
+ // "tests/setup.ts",
34
+
35
35
  // "package.json",
36
36
  // "tsconfig.json",
37
37
  // "tsconfig.build.json",
@@ -43,7 +43,11 @@ const includeFiles: string[] = [
43
43
  // "src/tokenCache.ts",
44
44
  // "src/generated/runtime.ts",
45
45
  // "src/quickbaseClient.ts",
46
+ // "src/invokeMethod.ts",
46
47
  // "src/generated-unified/QuickbaseClient.ts",
48
+
49
+ // "src/RateLimitError.ts",
50
+ // "src/TokenBucket.ts"
47
51
  ];
48
52
 
49
53
  const projectGoals: string[] = [