quickbase-js 0.1.0-beta.8 → 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 (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +822 -119
  4. package/dist/esm/quickbase.js +373 -236
  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 +373 -236
  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/generate-unified-interface.ts +97 -17
  13. package/open-api/utils/generateJsDoc.ts +96 -0
  14. package/package.json +3 -1
  15. package/src/ThrottleBucket.ts +17 -12
  16. package/src/authorizationStrategy.ts +246 -0
  17. package/src/generated-unified/QuickbaseClient.ts +809 -119
  18. package/src/invokeMethod.ts +150 -265
  19. package/src/quickbaseClient.ts +85 -58
  20. package/src/rateLimiter.ts +25 -0
  21. package/src/tokenCache.ts +10 -26
  22. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  23. package/tests/vitest/qb/auth/retryUserToken401.test.ts +59 -42
  24. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  25. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  26. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  27. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  28. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  29. package/tests/vitest/qb/tables/updateRelationship.test.ts +118 -145
  30. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  31. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  32. package/tests/vitest/unit/apps/updateApp.test.ts +42 -11
  33. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +3 -2
  34. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  35. package/tests/vitest/unit/auth/retryTempToken401.test.ts +46 -64
  36. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  37. package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -43
  38. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  39. package/tests/vitest/unit/fields/createField.test.ts +180 -116
  40. package/tests/vitest/unit/fields/getFieldUsage.test.ts +17 -3
  41. package/tests/vitest/unit/fields/updateField.test.ts +12 -7
  42. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  43. package/tests/vitest/unit/index.ts +2 -1
  44. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  45. package/tests/vitest/unit/rateLimit.test.ts +4 -2
  46. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  47. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  48. package/tests/vitest/unit/tables/updateRelationship.test.ts +12 -10
  49. package/tests/vitest/unit/tokenLifespan.test.ts +149 -83
  50. package/tools/log-gen.ts +14 -5
  51. package/tools/log-gen.yaml +710 -268
  52. package/tests/vitest/unit/auth/retryTempTokenTwo401.test.ts +0 -81
@@ -1,56 +1,69 @@
1
- import { describe, it, vi, expect, beforeEach } from "vitest";
2
- import { quickbase, QuickbaseClient } from "../../../../src/quickbaseClient";
3
- import { ResponseError } from "../../../../src/generated/runtime";
1
+ // tests/vitest/unit/auth/retryUserToken401.test.ts
4
2
 
5
- describe("QuickbaseClient Integration - User Token Retry on 401", () => {
6
- let client: QuickbaseClient;
7
- const mockFetch = vi.fn();
8
-
9
- beforeEach(() => {
10
- const config = {
11
- realm: process.env.QB_REALM || "builderprogram-dbradford6815",
12
- userToken: "mock-user-token",
13
- debug: true,
14
- fetchApi: mockFetch,
15
- };
16
- console.log("[quickbaseTest] Config:", config);
17
- client = quickbase(config);
18
- });
19
-
20
- it("retries on transient 401 with user token and succeeds", async () => {
21
- const consoleSpy = vi.spyOn(console, "log");
3
+ import { describe, it, expect, vi } from "vitest";
4
+ import {
5
+ createClient,
6
+ mockFetch,
7
+ QB_REALM,
8
+ QB_TABLE_ID_1,
9
+ } from "@tests/setup.ts";
22
10
 
11
+ describe("QuickbaseClient Unit - User Token Retry on 401", () => {
12
+ it("retries with the same user token on 401 and succeeds", async () => {
13
+ const userToken = "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3";
23
14
  mockFetch
24
- .mockImplementationOnce(() => {
25
- const response = new Response(null, {
15
+ .mockResolvedValueOnce(
16
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
26
17
  status: 401,
27
- statusText: "Unauthorized",
28
- });
29
- console.log("[mockFetch] Throwing 401 ResponseError");
30
- return Promise.reject(new ResponseError(response));
31
- })
32
- .mockResolvedValueOnce({
33
- ok: true,
34
- status: 200,
35
- json: async () => [{ id: 1, label: "Mock Field", fieldType: "text" }],
36
- });
18
+ })
19
+ )
20
+ .mockResolvedValueOnce(
21
+ new Response(JSON.stringify([{ id: 1, label: "Field1" }]), {
22
+ status: 200,
23
+ })
24
+ );
37
25
 
38
- const fields = await client.getFields({ tableId: "mock-table-id" });
26
+ const consoleSpy = vi.spyOn(console, "log");
27
+ const client = createClient(mockFetch, {
28
+ realm: QB_REALM,
29
+ userToken,
30
+ useTempTokens: false,
31
+ debug: true,
32
+ });
39
33
 
40
- expect(fields).toBeInstanceOf(Array);
41
- expect(fields.length).toBeGreaterThan(0);
42
- expect(fields[0]).toHaveProperty("id");
43
- expect(fields[0]).toHaveProperty("label");
34
+ const result = await client.getFields({ tableId: QB_TABLE_ID_1 });
44
35
 
36
+ expect(result).toEqual([{ id: 1, label: "Field1" }]);
37
+ expect(mockFetch).toHaveBeenCalledTimes(2);
38
+ expect(mockFetch).toHaveBeenNthCalledWith(
39
+ 1,
40
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
41
+ expect.objectContaining({
42
+ method: "GET",
43
+ headers: expect.objectContaining({
44
+ Authorization: `QB-USER-TOKEN ${userToken}`,
45
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
46
+ "Content-Type": "application/json",
47
+ }),
48
+ })
49
+ );
50
+ expect(mockFetch).toHaveBeenNthCalledWith(
51
+ 2,
52
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
53
+ expect.objectContaining({
54
+ method: "GET",
55
+ headers: expect.objectContaining({
56
+ Authorization: `QB-USER-TOKEN ${userToken}`,
57
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
58
+ "Content-Type": "application/json",
59
+ }),
60
+ })
61
+ );
45
62
  expect(consoleSpy).toHaveBeenCalledWith(
46
- "Authorization error for getFields (user token), retrying with same token:",
47
- "" // Matches empty ResponseError message from mock
63
+ `Retrying getFields with existing user token: b9f3pk_q4j...`
48
64
  );
49
65
  expect(consoleSpy).toHaveBeenCalledWith(
50
- "Retrying getFields with user token"
66
+ "[getFields] Retrying with token: b9f3pk_q4j..."
51
67
  );
52
- expect(mockFetch).toHaveBeenCalledTimes(2);
53
-
54
- consoleSpy.mockRestore();
55
68
  });
56
69
  });
@@ -0,0 +1,325 @@
1
+ // tests/vitest/unit/auth/ssoTokenRefresh.test.ts
2
+
3
+ import { describe, it, expect, beforeEach, vi } from "vitest";
4
+ import { createClient, mockFetch, QB_REALM, QB_APP_ID } from "@tests/setup.ts";
5
+
6
+ describe("QuickbaseClient Unit - SsoTokenStrategy", () => {
7
+ let client: ReturnType<typeof createClient>;
8
+ const mockSamlToken = "mock_saml_token_base64url";
9
+ const mockSsoToken = "example_token_1NiIsImtpZCI6IjllciJ9w";
10
+
11
+ beforeEach(() => {
12
+ mockFetch.mockClear();
13
+ client = createClient(mockFetch, {
14
+ useSso: true,
15
+ samlToken: mockSamlToken,
16
+ realm: QB_REALM,
17
+ debug: true,
18
+ });
19
+ });
20
+
21
+ it("initializes without errors", () => {
22
+ expect(client).toBeDefined();
23
+ });
24
+
25
+ it("has exchangeSsoToken method", () => {
26
+ expect(typeof client.exchangeSsoToken).toBe("function");
27
+ });
28
+
29
+ it("fetches SSO token on first call", async () => {
30
+ const mockResponse = {
31
+ access_token: mockSsoToken,
32
+ issued_token_type: "urn:quickbase:params:oauth:token-type:temp_token",
33
+ token_type: "N_A",
34
+ };
35
+
36
+ mockFetch.mockResolvedValueOnce(
37
+ new Response(JSON.stringify(mockResponse), {
38
+ status: 200,
39
+ headers: { "Content-Type": "application/json" },
40
+ })
41
+ );
42
+
43
+ mockFetch.mockResolvedValueOnce(
44
+ new Response(JSON.stringify({ id: QB_APP_ID, name: "Test App" }), {
45
+ status: 200,
46
+ headers: { "Content-Type": "application/json" },
47
+ })
48
+ );
49
+
50
+ const result = await client.getApp({ appId: QB_APP_ID });
51
+
52
+ expect(mockFetch).toHaveBeenCalledTimes(2);
53
+ expect(mockFetch).toHaveBeenNthCalledWith(
54
+ 1,
55
+ "https://api.quickbase.com/v1/auth/oauth/token",
56
+ expect.objectContaining({
57
+ method: "POST",
58
+ headers: expect.objectContaining({
59
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
60
+ "Content-Type": "application/json",
61
+ }),
62
+ body: JSON.stringify({
63
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
64
+ requested_token_type:
65
+ "urn:quickbase:params:oauth:token-type:temp_token",
66
+ subject_token: mockSamlToken,
67
+ subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
68
+ }),
69
+ })
70
+ );
71
+ expect(mockFetch).toHaveBeenNthCalledWith(
72
+ 2,
73
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
74
+ expect.objectContaining({
75
+ headers: expect.objectContaining({
76
+ Authorization: `QB-TEMP-TOKEN ${mockSsoToken}`,
77
+ }),
78
+ })
79
+ );
80
+ expect(result).toEqual({ id: QB_APP_ID, name: "Test App" });
81
+ });
82
+
83
+ it("refreshes SSO token on 401 and retries", async () => {
84
+ const newToken = "new_token_abcdef123";
85
+ const appResponse = { id: QB_APP_ID, name: "Test App" };
86
+
87
+ mockFetch
88
+ .mockResolvedValueOnce(
89
+ new Response(
90
+ JSON.stringify({
91
+ access_token: mockSsoToken,
92
+ issued_token_type:
93
+ "urn:quickbase:params:oauth:token-type:temp_token",
94
+ token_type: "N_A",
95
+ }),
96
+ { status: 200, headers: { "Content-Type": "application/json" } }
97
+ )
98
+ )
99
+ .mockResolvedValueOnce(
100
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
101
+ status: 401,
102
+ headers: { "Content-Type": "application/json" },
103
+ })
104
+ )
105
+ .mockResolvedValueOnce(
106
+ new Response(
107
+ JSON.stringify({
108
+ access_token: newToken,
109
+ issued_token_type:
110
+ "urn:quickbase:params:oauth:token-type:temp_token",
111
+ token_type: "N_A",
112
+ }),
113
+ { status: 200, headers: { "Content-Type": "application/json" } }
114
+ )
115
+ )
116
+ .mockResolvedValueOnce(
117
+ new Response(JSON.stringify(appResponse), {
118
+ status: 200,
119
+ headers: { "Content-Type": "application/json" },
120
+ })
121
+ );
122
+
123
+ const result = await client.getApp({ appId: QB_APP_ID });
124
+
125
+ expect(result).toEqual(appResponse);
126
+ expect(mockFetch).toHaveBeenCalledTimes(4);
127
+ expect(mockFetch).toHaveBeenNthCalledWith(
128
+ 1,
129
+ "https://api.quickbase.com/v1/auth/oauth/token",
130
+ expect.any(Object)
131
+ );
132
+ expect(mockFetch).toHaveBeenNthCalledWith(
133
+ 2,
134
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
135
+ expect.objectContaining({
136
+ headers: expect.objectContaining({
137
+ Authorization: `QB-TEMP-TOKEN ${mockSsoToken}`,
138
+ }),
139
+ })
140
+ );
141
+ expect(mockFetch).toHaveBeenNthCalledWith(
142
+ 3,
143
+ "https://api.quickbase.com/v1/auth/oauth/token",
144
+ expect.any(Object)
145
+ );
146
+ expect(mockFetch).toHaveBeenNthCalledWith(
147
+ 4,
148
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
149
+ expect.objectContaining({
150
+ headers: expect.objectContaining({
151
+ Authorization: `QB-TEMP-TOKEN ${newToken}`,
152
+ }),
153
+ })
154
+ );
155
+ });
156
+
157
+ it("handles API error on token exchange", async () => {
158
+ // Reset client to avoid interference from beforeEach
159
+ client = createClient(mockFetch, {
160
+ useSso: true,
161
+ samlToken: mockSamlToken,
162
+ realm: QB_REALM,
163
+ debug: true,
164
+ });
165
+
166
+ mockFetch
167
+ .mockResolvedValueOnce(
168
+ new Response(
169
+ JSON.stringify({
170
+ access_token: mockSsoToken,
171
+ issued_token_type:
172
+ "urn:quickbase:params:oauth:token-type:temp_token",
173
+ token_type: "N_A",
174
+ }),
175
+ { status: 200, headers: { "Content-Type": "application/json" } }
176
+ )
177
+ )
178
+ .mockResolvedValueOnce(
179
+ new Response(JSON.stringify({ message: "Invalid SAML token" }), {
180
+ status: 400,
181
+ headers: { "Content-Type": "application/json" },
182
+ })
183
+ );
184
+
185
+ await expect(
186
+ client.exchangeSsoToken({
187
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
188
+ requested_token_type:
189
+ "urn:quickbase:params:oauth:token-type:temp_token",
190
+ subject_token: mockSamlToken,
191
+ subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
192
+ })
193
+ ).rejects.toThrow("API Error: Invalid SAML token (Status: 400)");
194
+
195
+ expect(mockFetch).toHaveBeenCalledTimes(2);
196
+ expect(mockFetch).toHaveBeenNthCalledWith(
197
+ 1,
198
+ "https://api.quickbase.com/v1/auth/oauth/token",
199
+ expect.any(Object)
200
+ );
201
+ expect(mockFetch).toHaveBeenNthCalledWith(
202
+ 2,
203
+ "https://api.quickbase.com/v1/auth/oauth/token",
204
+ expect.any(Object)
205
+ );
206
+ });
207
+
208
+ it("exhausts retries on repeated 401s", async () => {
209
+ mockFetch
210
+ .mockResolvedValueOnce(
211
+ new Response(
212
+ JSON.stringify({
213
+ access_token: mockSsoToken,
214
+ issued_token_type:
215
+ "urn:quickbase:params:oauth:token-type:temp_token",
216
+ token_type: "N_A",
217
+ }),
218
+ { status: 200, headers: { "Content-Type": "application/json" } }
219
+ )
220
+ )
221
+ .mockResolvedValueOnce(
222
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
223
+ status: 401,
224
+ headers: { "Content-Type": "application/json" },
225
+ })
226
+ )
227
+ .mockResolvedValueOnce(
228
+ new Response(
229
+ JSON.stringify({
230
+ access_token: "retry_token_1",
231
+ issued_token_type:
232
+ "urn:quickbase:params:oauth:token-type:temp_token",
233
+ token_type: "N_A",
234
+ }),
235
+ { status: 200, headers: { "Content-Type": "application/json" } }
236
+ )
237
+ )
238
+ .mockResolvedValueOnce(
239
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
240
+ status: 401,
241
+ headers: { "Content-Type": "application/json" },
242
+ })
243
+ )
244
+ .mockResolvedValueOnce(
245
+ new Response(
246
+ JSON.stringify({
247
+ access_token: "retry_token_2",
248
+ issued_token_type:
249
+ "urn:quickbase:params:oauth:token-type:temp_token",
250
+ token_type: "N_A",
251
+ }),
252
+ { status: 200, headers: { "Content-Type": "application/json" } }
253
+ )
254
+ )
255
+ .mockResolvedValueOnce(
256
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
257
+ status: 401,
258
+ headers: { "Content-Type": "application/json" },
259
+ })
260
+ )
261
+ .mockResolvedValueOnce(
262
+ new Response(
263
+ JSON.stringify({
264
+ access_token: "retry_token_3",
265
+ issued_token_type:
266
+ "urn:quickbase:params:oauth:token-type:temp_token",
267
+ token_type: "N_A",
268
+ }),
269
+ { status: 200, headers: { "Content-Type": "application/json" } }
270
+ )
271
+ )
272
+ .mockResolvedValueOnce(
273
+ new Response(JSON.stringify({ message: "Unauthorized" }), {
274
+ status: 401,
275
+ headers: { "Content-Type": "application/json" },
276
+ })
277
+ );
278
+
279
+ await expect(client.getApp({ appId: QB_APP_ID })).rejects.toThrow(
280
+ "API Error: Unauthorized (Status: 401)"
281
+ );
282
+
283
+ expect(mockFetch).toHaveBeenCalledTimes(8);
284
+ expect(mockFetch).toHaveBeenNthCalledWith(
285
+ 1,
286
+ "https://api.quickbase.com/v1/auth/oauth/token",
287
+ expect.any(Object)
288
+ );
289
+ expect(mockFetch).toHaveBeenNthCalledWith(
290
+ 2,
291
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
292
+ expect.any(Object)
293
+ );
294
+ expect(mockFetch).toHaveBeenNthCalledWith(
295
+ 3,
296
+ "https://api.quickbase.com/v1/auth/oauth/token",
297
+ expect.any(Object)
298
+ );
299
+ expect(mockFetch).toHaveBeenNthCalledWith(
300
+ 4,
301
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
302
+ expect.any(Object)
303
+ );
304
+ expect(mockFetch).toHaveBeenNthCalledWith(
305
+ 5,
306
+ "https://api.quickbase.com/v1/auth/oauth/token",
307
+ expect.any(Object)
308
+ );
309
+ expect(mockFetch).toHaveBeenNthCalledWith(
310
+ 6,
311
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
312
+ expect.any(Object)
313
+ );
314
+ expect(mockFetch).toHaveBeenNthCalledWith(
315
+ 7,
316
+ "https://api.quickbase.com/v1/auth/oauth/token",
317
+ expect.any(Object)
318
+ );
319
+ expect(mockFetch).toHaveBeenNthCalledWith(
320
+ 8,
321
+ `https://api.quickbase.com/v1/apps/${QB_APP_ID}`,
322
+ expect.any(Object)
323
+ );
324
+ });
325
+ });