quickbase-js 0.1.0-beta.10

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 (165) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/LICENSE +21 -0
  3. package/README.md +179 -0
  4. package/api-examples.md +49 -0
  5. package/build-common.js +131 -0
  6. package/build-esm.js +71 -0
  7. package/build-umd.js +69 -0
  8. package/dist/esm/quickbase.d.ts +8081 -0
  9. package/dist/esm/quickbase.js +7928 -0
  10. package/dist/esm/quickbase.js.map +1 -0
  11. package/dist/esm/quickbase.min.js +2 -0
  12. package/dist/esm/quickbase.min.js.map +1 -0
  13. package/dist/umd/quickbase.umd.js +8504 -0
  14. package/dist/umd/quickbase.umd.js.map +1 -0
  15. package/dist/umd/quickbase.umd.min.js +2 -0
  16. package/dist/umd/quickbase.umd.min.js.map +1 -0
  17. package/docs/.vitepress/cache/deps/@theme_index.js +275 -0
  18. package/docs/.vitepress/cache/deps/@theme_index.js.map +7 -0
  19. package/docs/.vitepress/cache/deps/_metadata.json +40 -0
  20. package/docs/.vitepress/cache/deps/chunk-5A67QEFC.js +12542 -0
  21. package/docs/.vitepress/cache/deps/chunk-5A67QEFC.js.map +7 -0
  22. package/docs/.vitepress/cache/deps/chunk-7D4P4MMP.js +9719 -0
  23. package/docs/.vitepress/cache/deps/chunk-7D4P4MMP.js.map +7 -0
  24. package/docs/.vitepress/cache/deps/package.json +3 -0
  25. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4504 -0
  26. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
  27. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +583 -0
  28. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
  29. package/docs/.vitepress/cache/deps/vue.js +343 -0
  30. package/docs/.vitepress/cache/deps/vue.js.map +7 -0
  31. package/docs/.vitepress/config.mts +26 -0
  32. package/docs/api-examples.md +49 -0
  33. package/docs/index.md +59 -0
  34. package/docs/markdown-examples.md +85 -0
  35. package/docs-data/api-docs.json +4143 -0
  36. package/examples/quicbase-jsCDN.html +36 -0
  37. package/examples/react.js +48 -0
  38. package/examples/vanillajs.html +47 -0
  39. package/index.md +25 -0
  40. package/markdown-examples.md +85 -0
  41. package/open-api/fix-spec-main.ts +133 -0
  42. package/open-api/generate-unified-interface.ts +104 -0
  43. package/open-api/generateDocsJson.ts +24 -0
  44. package/open-api/output/quickbase-fixed.json +11191 -0
  45. package/open-api/post-process-generated.ts +226 -0
  46. package/open-api/regenerate-client.ts +117 -0
  47. package/open-api/schema/enhance-general.ts +111 -0
  48. package/open-api/schema/fix-arrays.ts +68 -0
  49. package/open-api/schema/tags/apps.ts +90 -0
  50. package/open-api/schema/tags/fields.ts +471 -0
  51. package/open-api/schema/tags/groups.ts +51 -0
  52. package/open-api/schema/tags/index.ts +66 -0
  53. package/open-api/schema/tags/records.ts +115 -0
  54. package/open-api/schema/tags/tables.ts +211 -0
  55. package/open-api/schema/tags/users.ts +47 -0
  56. package/open-api/types/spec.ts +42 -0
  57. package/open-api/utils/common.ts +29 -0
  58. package/open-api/utils/generateJsDoc.ts +74 -0
  59. package/open-api/utils/infer-schema.ts +40 -0
  60. package/open-api/utils/naming.ts +10 -0
  61. package/open-api/utils/sharedUtils.ts +352 -0
  62. package/package.json +96 -0
  63. package/playwright.config.ts +37 -0
  64. package/rollup.config.js +114 -0
  65. package/specs/QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json +46878 -0
  66. package/specs/split/apps.json +1315 -0
  67. package/specs/split/auth.json +191 -0
  68. package/specs/split/fields.json +3869 -0
  69. package/specs/split/records.json +754 -0
  70. package/specs/split/reports.json +619 -0
  71. package/specs/split/tables.json +1605 -0
  72. package/src/RateLimitError.ts +12 -0
  73. package/src/ThrottleBucket.ts +41 -0
  74. package/src/authorizationStrategy.ts +283 -0
  75. package/src/generated-unified/QuickbaseClient.ts +1135 -0
  76. package/src/generated-unified/missing-types-report.json +3 -0
  77. package/src/index.ts +1 -0
  78. package/src/invokeMethod.ts +245 -0
  79. package/src/quickbaseClient.ts +194 -0
  80. package/src/rateLimiter.ts +25 -0
  81. package/src/tokenCache.ts +35 -0
  82. package/src/utils.ts +50 -0
  83. package/tests/playwright/qb/auth/cacheSwitching.test.ts +159 -0
  84. package/tests/playwright/qb/auth/fetchTempToken401.test.ts +219 -0
  85. package/tests/playwright/qb/auth/getAppWithTempToken.test.ts +162 -0
  86. package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +153 -0
  87. package/tests/playwright/qb/auth/getTempToken.test.ts +152 -0
  88. package/tests/playwright/qb/auth/tempTokenPrefetch.test.ts +179 -0
  89. package/tests/playwright/qb/auth/tempTokenRenewal.test.ts +133 -0
  90. package/tests/setup.ts +53 -0
  91. package/tests/vitest/qb/apps/copyApp.test.ts +76 -0
  92. package/tests/vitest/qb/apps/createApp.test.ts +64 -0
  93. package/tests/vitest/qb/apps/deleteApp.test.ts +76 -0
  94. package/tests/vitest/qb/apps/getApp.test.ts +44 -0
  95. package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
  96. package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
  97. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  98. package/tests/vitest/qb/auth/retryUserToken401.test.ts +73 -0
  99. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  100. package/tests/vitest/qb/fields/createField.test.ts +77 -0
  101. package/tests/vitest/qb/fields/deleteFields.test.ts +83 -0
  102. package/tests/vitest/qb/fields/getField.test.ts +85 -0
  103. package/tests/vitest/qb/fields/getFieldUsage.test.ts +82 -0
  104. package/tests/vitest/qb/fields/getFields.test.ts +232 -0
  105. package/tests/vitest/qb/fields/getFieldsUsage.test.ts +77 -0
  106. package/tests/vitest/qb/fields/updateField.test.ts +93 -0
  107. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  108. package/tests/vitest/qb/index.ts +29 -0
  109. package/tests/vitest/qb/records/deleteRecords.test.ts +110 -0
  110. package/tests/vitest/qb/records/runQuery.test.ts +146 -0
  111. package/tests/vitest/qb/records/upsert.test.ts +102 -0
  112. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  113. package/tests/vitest/qb/tables/createTable.test.ts +55 -0
  114. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  115. package/tests/vitest/qb/tables/deleteTable.test.ts +50 -0
  116. package/tests/vitest/qb/tables/getAppTables.test.ts +46 -0
  117. package/tests/vitest/qb/tables/getRelationships.test.ts +80 -0
  118. package/tests/vitest/qb/tables/getTable.test.ts +26 -0
  119. package/tests/vitest/qb/tables/updateRelationship.test.ts +250 -0
  120. package/tests/vitest/qb/tables/updateTable.test.ts +48 -0
  121. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  122. package/tests/vitest/unit/apps/copyApp.test.ts +581 -0
  123. package/tests/vitest/unit/apps/createApp.test.ts +186 -0
  124. package/tests/vitest/unit/apps/deleteApp.test.ts +216 -0
  125. package/tests/vitest/unit/apps/getApp.test.ts +128 -0
  126. package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
  127. package/tests/vitest/unit/apps/updateApp.test.ts +412 -0
  128. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +63 -0
  129. package/tests/vitest/unit/auth/getTempToken.test.ts +119 -0
  130. package/tests/vitest/unit/auth/retryTempToken401.test.ts +95 -0
  131. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +206 -0
  132. package/tests/vitest/unit/auth/retryUserToken401.test.ts +69 -0
  133. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  134. package/tests/vitest/unit/fields/createField.test.ts +451 -0
  135. package/tests/vitest/unit/fields/deleteFields.test.ts +141 -0
  136. package/tests/vitest/unit/fields/getField.test.ts +189 -0
  137. package/tests/vitest/unit/fields/getFieldUsage.test.ts +180 -0
  138. package/tests/vitest/unit/fields/getFields.test.ts +122 -0
  139. package/tests/vitest/unit/fields/getFieldsUsage.test.ts +160 -0
  140. package/tests/vitest/unit/fields/updateField.test.ts +285 -0
  141. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  142. package/tests/vitest/unit/index.ts +34 -0
  143. package/tests/vitest/unit/quickbaseClientHttpMethods.test.ts +139 -0
  144. package/tests/vitest/unit/rateLimit.test.ts +191 -0
  145. package/tests/vitest/unit/records/deleteRecords.test.ts +112 -0
  146. package/tests/vitest/unit/records/runQuery.test.ts +102 -0
  147. package/tests/vitest/unit/records/upsert.test.ts +234 -0
  148. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  149. package/tests/vitest/unit/tables/createTable.test.ts +70 -0
  150. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  151. package/tests/vitest/unit/tables/deleteTable.test.ts +77 -0
  152. package/tests/vitest/unit/tables/getAppTables.test.ts +85 -0
  153. package/tests/vitest/unit/tables/getRelationships.test.ts +259 -0
  154. package/tests/vitest/unit/tables/getTable.test.ts +233 -0
  155. package/tests/vitest/unit/tables/updateRelationship.test.ts +307 -0
  156. package/tests/vitest/unit/tables/updateTable.test.ts +80 -0
  157. package/tests/vitest/unit/tokenLifespan.test.ts +204 -0
  158. package/tests/vitest/unit/umd.test.ts +8 -0
  159. package/tools/log-gen.ts +251 -0
  160. package/tools/log-gen.yaml +4777 -0
  161. package/tools/openapi-jars/openapi-generator-cli-7.12.0.jar +0 -0
  162. package/tsconfig.build.json +17 -0
  163. package/tsconfig.json +32 -0
  164. package/versionrc.json +9 -0
  165. package/vitest.config.ts +27 -0
@@ -0,0 +1,307 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import {
3
+ createClient,
4
+ mockFetch,
5
+ QB_REALM,
6
+ QB_USER_TOKEN,
7
+ QB_TABLE_ID_1,
8
+ } from "@tests/setup.ts";
9
+ import {
10
+ UpdateRelationshipRequest,
11
+ UpdateRelationship200Response,
12
+ } from "@/generated/models";
13
+
14
+ describe("QuickbaseClient Unit - updateRelationship", () => {
15
+ let client: ReturnType<typeof createClient>;
16
+
17
+ beforeEach(() => {
18
+ mockFetch.mockClear();
19
+ });
20
+
21
+ it("initializes without errors and has updateRelationship method", () => {
22
+ client = createClient(mockFetch, { debug: true });
23
+ expect(client).toBeDefined();
24
+ expect(typeof client.updateRelationship).toBe("function");
25
+ });
26
+
27
+ it("updates relationship successfully with user token", async () => {
28
+ client = createClient(mockFetch, { debug: true });
29
+
30
+ const tableId = QB_TABLE_ID_1;
31
+ const relationshipId = 6;
32
+
33
+ const request: UpdateRelationshipRequest = {
34
+ lookupFieldIds: [4, 5, 6],
35
+ summaryFields: [
36
+ {
37
+ summaryFid: 3,
38
+ label: "my summary field",
39
+ accumulationType: "COUNT",
40
+ where: "{'3'.EX.'1'}",
41
+ },
42
+ ],
43
+ };
44
+
45
+ const mockResponse: UpdateRelationship200Response = {
46
+ id: relationshipId,
47
+ parentTableId: "bck7gp3q2",
48
+ childTableId: tableId,
49
+ foreignKeyField: {
50
+ id: relationshipId,
51
+ label: "Related record",
52
+ type: "numeric",
53
+ },
54
+ isCrossApp: false,
55
+ lookupFields: [
56
+ { id: 4, label: "Field 4", type: "text" },
57
+ { id: 5, label: "Field 5", type: "numeric" },
58
+ { id: 6, label: "Field 6", type: "date" },
59
+ ],
60
+ summaryFields: [{ id: 8, label: "my summary field", type: "numeric" }],
61
+ };
62
+
63
+ mockFetch.mockResolvedValueOnce({
64
+ ok: true,
65
+ status: 200,
66
+ json: () => Promise.resolve(mockResponse),
67
+ });
68
+
69
+ const result = await client.updateRelationship({
70
+ tableId,
71
+ relationshipId,
72
+ body: request,
73
+ });
74
+
75
+ expect(result).toEqual(mockResponse);
76
+ expect(mockFetch).toHaveBeenCalledWith(
77
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
78
+ expect.objectContaining({
79
+ method: "POST",
80
+ headers: expect.objectContaining({
81
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
82
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
83
+ "Content-Type": "application/json",
84
+ }),
85
+ body: JSON.stringify(request),
86
+ credentials: "omit",
87
+ })
88
+ );
89
+ });
90
+
91
+ it("updates relationship successfully with temp token", async () => {
92
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
93
+
94
+ const tableId = QB_TABLE_ID_1;
95
+ const relationshipId = 6;
96
+
97
+ const request: UpdateRelationshipRequest = {
98
+ lookupFieldIds: [7],
99
+ summaryFields: [
100
+ {
101
+ summaryFid: 0,
102
+ label: "Count of Records",
103
+ accumulationType: "COUNT",
104
+ where: "{'3'.GT.0}",
105
+ },
106
+ ],
107
+ };
108
+
109
+ const mockResponse: UpdateRelationship200Response = {
110
+ id: relationshipId,
111
+ parentTableId: "bck7gp3q2",
112
+ childTableId: tableId,
113
+ foreignKeyField: {
114
+ id: relationshipId,
115
+ label: "Related record",
116
+ type: "numeric",
117
+ },
118
+ isCrossApp: false,
119
+ lookupFields: [{ id: 7, label: "record - text field", type: "text" }],
120
+ summaryFields: [{ id: 9, label: "Count of Records", type: "numeric" }],
121
+ };
122
+
123
+ mockFetch
124
+ .mockResolvedValueOnce({
125
+ ok: true,
126
+ status: 200,
127
+ json: () => Promise.resolve({ temporaryAuthorization: "temp_token" }),
128
+ })
129
+ .mockResolvedValueOnce({
130
+ ok: true,
131
+ status: 200,
132
+ json: () => Promise.resolve(mockResponse),
133
+ });
134
+
135
+ const result = await client.updateRelationship({
136
+ tableId,
137
+ relationshipId,
138
+ body: request,
139
+ });
140
+
141
+ expect(result).toEqual(mockResponse);
142
+ expect(mockFetch).toHaveBeenCalledTimes(2);
143
+ expect(mockFetch).toHaveBeenNthCalledWith(
144
+ 1,
145
+ `https://api.quickbase.com/v1/auth/temporary/${tableId}`,
146
+ expect.objectContaining({
147
+ method: "GET",
148
+ headers: expect.objectContaining({
149
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
150
+ "Content-Type": "application/json",
151
+ }),
152
+ credentials: "include",
153
+ })
154
+ );
155
+ expect(mockFetch).toHaveBeenNthCalledWith(
156
+ 2,
157
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
158
+ expect.objectContaining({
159
+ method: "POST",
160
+ headers: expect.objectContaining({
161
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
162
+ Authorization: "QB-TEMP-TOKEN temp_token",
163
+ "Content-Type": "application/json",
164
+ }),
165
+ body: JSON.stringify(request),
166
+ credentials: "omit",
167
+ })
168
+ );
169
+ });
170
+
171
+ it("retries successfully after 401 with temp token", async () => {
172
+ client = createClient(mockFetch, { useTempTokens: true, debug: true });
173
+
174
+ const tableId = QB_TABLE_ID_1;
175
+ const relationshipId = 6;
176
+
177
+ const request: UpdateRelationshipRequest = {
178
+ lookupFieldIds: [4],
179
+ };
180
+
181
+ const mockResponse: UpdateRelationship200Response = {
182
+ id: relationshipId,
183
+ parentTableId: "bck7gp3q2",
184
+ childTableId: tableId,
185
+ foreignKeyField: {
186
+ id: relationshipId,
187
+ label: "Related record",
188
+ type: "numeric",
189
+ },
190
+ isCrossApp: false,
191
+ lookupFields: [{ id: 4, label: "Field 4", type: "text" }],
192
+ summaryFields: [],
193
+ };
194
+
195
+ mockFetch
196
+ .mockResolvedValueOnce({
197
+ ok: true,
198
+ status: 200,
199
+ json: () =>
200
+ Promise.resolve({ temporaryAuthorization: "initial_token" }),
201
+ })
202
+ .mockResolvedValueOnce({
203
+ ok: false,
204
+ status: 401,
205
+ json: () => Promise.resolve({ message: "Unauthorized" }),
206
+ })
207
+ .mockResolvedValueOnce({
208
+ ok: true,
209
+ status: 200,
210
+ json: () => Promise.resolve({ temporaryAuthorization: "new_token" }),
211
+ })
212
+ .mockResolvedValueOnce({
213
+ ok: true,
214
+ status: 200,
215
+ json: () => Promise.resolve(mockResponse),
216
+ });
217
+
218
+ const consoleSpy = vi.spyOn(console, "log");
219
+ const result = await client.updateRelationship({
220
+ tableId,
221
+ relationshipId,
222
+ body: request,
223
+ });
224
+
225
+ expect(result).toEqual(mockResponse);
226
+ expect(mockFetch).toHaveBeenCalledTimes(4);
227
+ expect(mockFetch).toHaveBeenNthCalledWith(
228
+ 1,
229
+ `https://api.quickbase.com/v1/auth/temporary/${tableId}`,
230
+ expect.any(Object)
231
+ );
232
+ expect(mockFetch).toHaveBeenNthCalledWith(
233
+ 2,
234
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
235
+ expect.objectContaining({
236
+ headers: expect.objectContaining({
237
+ Authorization: "QB-TEMP-TOKEN initial_token",
238
+ }),
239
+ })
240
+ );
241
+ expect(mockFetch).toHaveBeenNthCalledWith(
242
+ 3,
243
+ `https://api.quickbase.com/v1/auth/temporary/${tableId}`,
244
+ expect.any(Object)
245
+ );
246
+ expect(mockFetch).toHaveBeenNthCalledWith(
247
+ 4,
248
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
249
+ expect.objectContaining({
250
+ headers: expect.objectContaining({
251
+ Authorization: "QB-TEMP-TOKEN new_token",
252
+ }),
253
+ })
254
+ );
255
+
256
+ // Updated log expectations
257
+ expect(consoleSpy).toHaveBeenCalledWith(
258
+ "Authorization error for updateRelationship (temp token), refreshing token:"
259
+ );
260
+ expect(consoleSpy).toHaveBeenCalledWith(
261
+ expect.stringContaining(
262
+ "[updateRelationship] Retrying with token: new_tok"
263
+ )
264
+ );
265
+
266
+ consoleSpy.mockRestore();
267
+ });
268
+
269
+ it("handles 400 error for invalid request", async () => {
270
+ client = createClient(mockFetch, { debug: true });
271
+
272
+ const tableId = QB_TABLE_ID_1;
273
+ const relationshipId = 6;
274
+
275
+ const request: UpdateRelationshipRequest = {
276
+ lookupFieldIds: [-1],
277
+ };
278
+
279
+ mockFetch.mockResolvedValueOnce({
280
+ ok: false,
281
+ status: 400,
282
+ json: () => Promise.resolve({ message: "Invalid field ID" }),
283
+ });
284
+
285
+ await expect(
286
+ client.updateRelationship({
287
+ tableId,
288
+ relationshipId,
289
+ body: request,
290
+ })
291
+ ).rejects.toThrow("API Error: Invalid field ID (Status: 400)");
292
+
293
+ expect(mockFetch).toHaveBeenCalledWith(
294
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
295
+ expect.objectContaining({
296
+ method: "POST",
297
+ headers: expect.objectContaining({
298
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
299
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
300
+ "Content-Type": "application/json",
301
+ }),
302
+ body: JSON.stringify(request),
303
+ credentials: "omit",
304
+ })
305
+ );
306
+ });
307
+ });
@@ -0,0 +1,80 @@
1
+ // tests/vitest/unit/records/updateTable.test.ts
2
+ import { describe, it, expect, beforeEach } from "vitest";
3
+ import {
4
+ createClient,
5
+ mockFetch,
6
+ QB_APP_ID,
7
+ QB_TABLE_ID_1,
8
+ } from "@tests/setup.ts";
9
+
10
+ describe("QuickbaseClient Unit - updateTable", () => {
11
+ let client: ReturnType<typeof createClient>;
12
+
13
+ beforeEach(() => {
14
+ mockFetch.mockClear();
15
+ client = createClient(mockFetch, { debug: true });
16
+ });
17
+
18
+ it("calls updateTable successfully with temp token", async () => {
19
+ client = createClient(mockFetch, { debug: true, useTempTokens: true });
20
+
21
+ const mockBody = {
22
+ name: "Updated Root",
23
+ description: "Updated description",
24
+ };
25
+ const mockResponse = {
26
+ id: QB_TABLE_ID_1,
27
+ name: "Updated Root",
28
+ alias: "_DBID_ROOT",
29
+ description: "Updated description",
30
+ created: new Date("2025-02-13T18:22:33.000Z"),
31
+ updated: new Date("2025-03-10T12:00:00.000Z"),
32
+ nextRecordId: 1,
33
+ nextFieldId: 6,
34
+ defaultSortFieldId: 2,
35
+ defaultSortOrder: "DESC",
36
+ keyFieldId: 3,
37
+ singleRecordName: "Root",
38
+ pluralRecordName: "Roots",
39
+ sizeLimit: "500 MB",
40
+ spaceUsed: "0 KB",
41
+ spaceRemaining: "500 MB",
42
+ };
43
+
44
+ mockFetch
45
+ .mockResolvedValueOnce({
46
+ ok: true,
47
+ status: 200,
48
+ json: () => Promise.resolve({ temporaryAuthorization: "temp_token" }),
49
+ })
50
+ .mockResolvedValueOnce({
51
+ ok: true,
52
+ status: 200,
53
+ json: () => Promise.resolve(mockResponse),
54
+ });
55
+
56
+ const response = await client.updateTable({
57
+ tableId: QB_TABLE_ID_1,
58
+ appId: QB_APP_ID,
59
+ body: mockBody,
60
+ });
61
+ expect(response).toEqual(mockResponse);
62
+ expect(mockFetch).toHaveBeenCalledTimes(2);
63
+ expect(mockFetch).toHaveBeenCalledWith(
64
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
65
+ expect.any(Object)
66
+ );
67
+ expect(mockFetch).toHaveBeenCalledWith(
68
+ `https://api.quickbase.com/v1/tables/${QB_TABLE_ID_1}?appId=${QB_APP_ID}`,
69
+ expect.objectContaining({
70
+ method: "POST",
71
+ headers: expect.objectContaining({
72
+ "QB-Realm-Hostname": `${process.env.QB_REALM}.quickbase.com`,
73
+ Authorization: "QB-TEMP-TOKEN temp_token",
74
+ "Content-Type": "application/json",
75
+ }),
76
+ body: JSON.stringify(mockBody),
77
+ })
78
+ );
79
+ });
80
+ });
@@ -0,0 +1,204 @@
1
+ import { describe, it, expect, beforeEach, afterEach, 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
+ const request: UpdateAppRequest = { name: "Test App" };
8
+ const mockResponse: UpdateApp200Response = {
9
+ id: QB_APP_ID,
10
+ name: "Test App",
11
+ description: "",
12
+ created: new Date("2020-01-01T00:00:00Z"), // Match deserialized Date object
13
+ updated: new Date("2020-01-01T00:00:00Z"),
14
+ dateFormat: "MM-DD-YYYY",
15
+ timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
16
+ hasEveryoneOnTheInternet: false,
17
+ ancestorId: undefined,
18
+ variables: [],
19
+ securityProperties: {},
20
+ memoryInfo: { estMemory: 0, estMemoryInclDependentApps: 0 },
21
+ dataClassification: "None",
22
+ };
23
+
24
+ beforeEach(() => {
25
+ mockFetch.mockClear();
26
+ vi.useFakeTimers();
27
+ console.log("[beforeEach] mockFetch cleared and fake timers enabled");
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.useRealTimers();
32
+ });
33
+
34
+ const setupMocks = (token: string) => {
35
+ mockFetch
36
+ .mockResolvedValueOnce({
37
+ ok: true,
38
+ status: 200,
39
+ json: () => Promise.resolve({ temporaryAuthorization: token }),
40
+ })
41
+ .mockResolvedValueOnce({
42
+ ok: true,
43
+ status: 200,
44
+ json: () => Promise.resolve(mockResponse),
45
+ });
46
+ };
47
+
48
+ it("reuses token within custom lifespan and refetches after expiration", async () => {
49
+ client = createClient(mockFetch, {
50
+ realm: QB_REALM,
51
+ useTempTokens: true,
52
+ tempTokenLifespan: 500,
53
+ debug: true,
54
+ });
55
+
56
+ setupMocks("token1");
57
+ await client.updateApp({ appId: QB_APP_ID, body: request });
58
+ expect(mockFetch).toHaveBeenCalledTimes(2);
59
+ expect(mockFetch.mock.calls[1][1].headers.Authorization).toBe(
60
+ "QB-TEMP-TOKEN token1"
61
+ );
62
+
63
+ mockFetch.mockResolvedValueOnce({
64
+ ok: true,
65
+ status: 200,
66
+ json: () => Promise.resolve(mockResponse),
67
+ });
68
+ await client.updateApp({ appId: QB_APP_ID, body: request });
69
+ expect(mockFetch).toHaveBeenCalledTimes(3);
70
+ expect(mockFetch.mock.calls[2][1].headers.Authorization).toBe(
71
+ "QB-TEMP-TOKEN token1"
72
+ );
73
+
74
+ vi.advanceTimersByTime(500);
75
+ console.log("[Test 1] Token expired after 500ms");
76
+
77
+ setupMocks("token2");
78
+ await client.updateApp({ appId: QB_APP_ID, body: request });
79
+ expect(mockFetch).toHaveBeenCalledTimes(5);
80
+ expect(mockFetch.mock.calls[4][1].headers.Authorization).toBe(
81
+ "QB-TEMP-TOKEN token2"
82
+ );
83
+ });
84
+
85
+ it("reuses token within default lifespan of 4m50s", async () => {
86
+ client = createClient(mockFetch, {
87
+ realm: QB_REALM,
88
+ useTempTokens: true,
89
+ debug: true,
90
+ });
91
+
92
+ setupMocks("default_token");
93
+ await client.updateApp({ appId: QB_APP_ID, body: request });
94
+ expect(mockFetch).toHaveBeenCalledTimes(2);
95
+ expect(mockFetch.mock.calls[1][1].headers.Authorization).toBe(
96
+ "QB-TEMP-TOKEN default_token"
97
+ );
98
+
99
+ vi.advanceTimersByTime(1000);
100
+ console.log(
101
+ "[Test 2] Advanced time by 1000ms, still within default lifespan"
102
+ );
103
+
104
+ mockFetch.mockResolvedValueOnce({
105
+ ok: true,
106
+ status: 200,
107
+ json: () => Promise.resolve(mockResponse),
108
+ });
109
+ await client.updateApp({ appId: QB_APP_ID, body: request });
110
+ expect(mockFetch).toHaveBeenCalledTimes(3);
111
+ expect(mockFetch.mock.calls[2][1].headers.Authorization).toBe(
112
+ "QB-TEMP-TOKEN default_token"
113
+ );
114
+ });
115
+
116
+ it("handles token fetch failure gracefully", async () => {
117
+ client = createClient(mockFetch, {
118
+ realm: QB_REALM,
119
+ useTempTokens: true,
120
+ tempTokenLifespan: 500,
121
+ });
122
+
123
+ mockFetch.mockResolvedValueOnce({
124
+ ok: false,
125
+ status: 500,
126
+ json: () => Promise.resolve({ message: "Network error" }),
127
+ });
128
+ await expect(
129
+ client.updateApp({ appId: QB_APP_ID, body: request })
130
+ ).rejects.toThrow("API Error: Network error (Status: 500)");
131
+ expect(mockFetch).toHaveBeenCalledTimes(1);
132
+ });
133
+
134
+ it("fetches new token every call with zero lifespan", async () => {
135
+ client = createClient(mockFetch, {
136
+ realm: QB_REALM,
137
+ useTempTokens: true,
138
+ tempTokenLifespan: 0,
139
+ });
140
+
141
+ setupMocks("token1");
142
+ await client.updateApp({ appId: QB_APP_ID, body: request });
143
+ expect(mockFetch).toHaveBeenCalledTimes(2);
144
+
145
+ setupMocks("token2");
146
+ await client.updateApp({ appId: QB_APP_ID, body: request });
147
+ expect(mockFetch).toHaveBeenCalledTimes(4);
148
+ expect(mockFetch.mock.calls[3][1].headers.Authorization).toBe(
149
+ "QB-TEMP-TOKEN token2"
150
+ );
151
+ });
152
+
153
+ it("reuses token across concurrent calls within lifespan", async () => {
154
+ client = createClient(mockFetch, {
155
+ realm: QB_REALM,
156
+ useTempTokens: true,
157
+ tempTokenLifespan: 500,
158
+ debug: true,
159
+ });
160
+
161
+ // Pre-fetch token to ensure it’s cached
162
+ mockFetch.mockResolvedValueOnce({
163
+ ok: true,
164
+ status: 200,
165
+ json: () => Promise.resolve({ temporaryAuthorization: "token1" }),
166
+ });
167
+ mockFetch.mockResolvedValueOnce({
168
+ ok: true,
169
+ status: 200,
170
+ json: () => Promise.resolve(mockResponse),
171
+ });
172
+ await client.updateApp({ appId: QB_APP_ID, body: request }); // Cache token1
173
+ vi.advanceTimersByTime(1);
174
+
175
+ // Reset mocks and set up API responses
176
+ mockFetch.mockClear();
177
+ mockFetch
178
+ .mockResolvedValueOnce({
179
+ ok: true,
180
+ status: 200,
181
+ json: () => Promise.resolve(mockResponse),
182
+ })
183
+ .mockResolvedValueOnce({
184
+ ok: true,
185
+ status: 200,
186
+ json: () => Promise.resolve(mockResponse),
187
+ });
188
+
189
+ const [call1, call2] = await Promise.all([
190
+ client.updateApp({ appId: QB_APP_ID, body: request }),
191
+ client.updateApp({ appId: QB_APP_ID, body: request }),
192
+ ]);
193
+
194
+ expect(mockFetch).toHaveBeenCalledTimes(2);
195
+ expect(mockFetch.mock.calls[0][1].headers.Authorization).toBe(
196
+ "QB-TEMP-TOKEN token1"
197
+ );
198
+ expect(mockFetch.mock.calls[1][1].headers.Authorization).toBe(
199
+ "QB-TEMP-TOKEN token1"
200
+ );
201
+ expect(call1).toEqual(mockResponse);
202
+ expect(call2).toEqual(mockResponse);
203
+ });
204
+ });
@@ -0,0 +1,8 @@
1
+ // tests/vitest/unit/umd.test.ts
2
+ import { test, expect } from "vitest";
3
+ const quickbaseUMD = require("../../dist/umd/quickbase.umd.js");
4
+ const { quickbase: quickbaseESM } = await import("../../dist/esm/quickbase.js");
5
+ test("UMD and ESM builds work in Node.js", () => {
6
+ expect(quickbaseUMD).toBeDefined();
7
+ expect(quickbaseESM).toBeDefined();
8
+ });