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,5 +1,3 @@
1
- // tests/vitest/unit/tables/updateRelationship.test.ts
2
-
3
1
  import { describe, it, expect, beforeEach, vi } from "vitest";
4
2
  import {
5
3
  createClient,
@@ -76,7 +74,7 @@ describe("QuickbaseClient Unit - updateRelationship", () => {
76
74
 
77
75
  expect(result).toEqual(mockResponse);
78
76
  expect(mockFetch).toHaveBeenCalledWith(
79
- `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
77
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
80
78
  expect.objectContaining({
81
79
  method: "POST",
82
80
  headers: expect.objectContaining({
@@ -156,7 +154,7 @@ describe("QuickbaseClient Unit - updateRelationship", () => {
156
154
  );
157
155
  expect(mockFetch).toHaveBeenNthCalledWith(
158
156
  2,
159
- `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
157
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
160
158
  expect.objectContaining({
161
159
  method: "POST",
162
160
  headers: expect.objectContaining({
@@ -233,7 +231,7 @@ describe("QuickbaseClient Unit - updateRelationship", () => {
233
231
  );
234
232
  expect(mockFetch).toHaveBeenNthCalledWith(
235
233
  2,
236
- `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
234
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
237
235
  expect.objectContaining({
238
236
  headers: expect.objectContaining({
239
237
  Authorization: "QB-TEMP-TOKEN initial_token",
@@ -247,20 +245,24 @@ describe("QuickbaseClient Unit - updateRelationship", () => {
247
245
  );
248
246
  expect(mockFetch).toHaveBeenNthCalledWith(
249
247
  4,
250
- `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
248
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
251
249
  expect.objectContaining({
252
250
  headers: expect.objectContaining({
253
251
  Authorization: "QB-TEMP-TOKEN new_token",
254
252
  }),
255
253
  })
256
254
  );
255
+
256
+ // Updated log expectations
257
257
  expect(consoleSpy).toHaveBeenCalledWith(
258
- "Authorization error for updateRelationship (temp token), refreshing token:",
259
- expect.any(String)
258
+ "Authorization error for updateRelationship (temp token), refreshing token:"
260
259
  );
261
260
  expect(consoleSpy).toHaveBeenCalledWith(
262
- "Retrying updateRelationship with temp token"
261
+ expect.stringContaining(
262
+ "[updateRelationship] Retrying with token: new_tok"
263
+ )
263
264
  );
265
+
264
266
  consoleSpy.mockRestore();
265
267
  });
266
268
 
@@ -289,7 +291,7 @@ describe("QuickbaseClient Unit - updateRelationship", () => {
289
291
  ).rejects.toThrow("API Error: Invalid field ID (Status: 400)");
290
292
 
291
293
  expect(mockFetch).toHaveBeenCalledWith(
292
- `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`, // Changed to singular
294
+ `https://api.quickbase.com/v1/tables/${tableId}/relationship/${relationshipId}`,
293
295
  expect.objectContaining({
294
296
  method: "POST",
295
297
  headers: expect.objectContaining({
@@ -1,120 +1,184 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
2
  import { createClient, mockFetch, QB_APP_ID, QB_REALM } from "@tests/setup.ts";
3
3
  import { UpdateAppRequest, UpdateApp200Response } from "@/generated/models";
4
4
 
5
5
  describe("QuickbaseClient Unit - Token Lifespan", () => {
6
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
+ };
7
23
 
8
24
  beforeEach(() => {
9
25
  mockFetch.mockClear();
26
+ vi.useFakeTimers();
27
+ console.log("[beforeEach] mockFetch cleared and fake timers enabled");
10
28
  });
11
29
 
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
- });
30
+ afterEach(() => {
31
+ vi.useRealTimers();
32
+ });
20
33
 
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
34
+ const setupMocks = (token: string) => {
41
35
  mockFetch
42
36
  .mockResolvedValueOnce({
43
37
  ok: true,
44
38
  status: 200,
45
- json: () => Promise.resolve({ temporaryAuthorization: "token1" }),
39
+ json: () => Promise.resolve({ temporaryAuthorization: token }),
46
40
  })
47
41
  .mockResolvedValueOnce({
48
42
  ok: true,
49
43
  status: 200,
50
44
  json: () => Promise.resolve(mockResponse),
51
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
+ });
52
55
 
56
+ setupMocks("token1");
53
57
  await client.updateApp({ appId: QB_APP_ID, body: request });
54
- expect(mockFetch).toHaveBeenCalledTimes(2); // Token fetch + API call
58
+ expect(mockFetch).toHaveBeenCalledTimes(2);
59
+ expect(mockFetch.mock.calls[1][1].headers.Authorization).toBe(
60
+ "QB-TEMP-TOKEN token1"
61
+ );
55
62
 
56
- // Second call within lifespan: Should reuse token1
57
63
  mockFetch.mockResolvedValueOnce({
58
64
  ok: true,
59
65
  status: 200,
60
66
  json: () => Promise.resolve(mockResponse),
61
67
  });
62
68
  await client.updateApp({ appId: QB_APP_ID, body: request });
63
- expect(mockFetch).toHaveBeenCalledTimes(3); // Only API call, no new token fetch
69
+ expect(mockFetch).toHaveBeenCalledTimes(3);
70
+ expect(mockFetch.mock.calls[2][1].headers.Authorization).toBe(
71
+ "QB-TEMP-TOKEN token1"
72
+ );
64
73
 
65
- // Wait for token to expire (500ms + buffer)
66
- await new Promise((resolve) => setTimeout(resolve, 600));
74
+ vi.advanceTimersByTime(500);
75
+ console.log("[Test 1] Token expired after 500ms");
67
76
 
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
- });
77
+ setupMocks("token2");
80
78
  await client.updateApp({ appId: QB_APP_ID, body: request });
81
- expect(mockFetch).toHaveBeenCalledTimes(5); // New token fetch + API call
79
+ expect(mockFetch).toHaveBeenCalledTimes(5);
80
+ expect(mockFetch.mock.calls[4][1].headers.Authorization).toBe(
81
+ "QB-TEMP-TOKEN token2"
82
+ );
82
83
  });
83
84
 
84
- it("uses default lifespan when not specified", async () => {
85
- // No tokenLifespan provided, should use default (4:50 = 290,000ms)
85
+ it("reuses token within default lifespan of 4m50s", async () => {
86
86
  client = createClient(mockFetch, {
87
87
  realm: QB_REALM,
88
88
  useTempTokens: true,
89
89
  debug: true,
90
90
  });
91
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
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();
112
177
  mockFetch
113
178
  .mockResolvedValueOnce({
114
179
  ok: true,
115
180
  status: 200,
116
- json: () =>
117
- Promise.resolve({ temporaryAuthorization: "default_token" }),
181
+ json: () => Promise.resolve(mockResponse),
118
182
  })
119
183
  .mockResolvedValueOnce({
120
184
  ok: true,
@@ -122,17 +186,19 @@ describe("QuickbaseClient Unit - Token Lifespan", () => {
122
186
  json: () => Promise.resolve(mockResponse),
123
187
  });
124
188
 
125
- await client.updateApp({ appId: QB_APP_ID, body: request });
126
- expect(mockFetch).toHaveBeenCalledTimes(2); // Token fetch + API call
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
+ ]);
127
193
 
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
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);
137
203
  });
138
204
  });
package/tools/log-gen.ts CHANGED
@@ -31,23 +31,32 @@ const includeRecursiveFolders: string[] = [
31
31
  // prettier-ignore
32
32
  const includeFiles: string[] = [
33
33
  // "tests/setup.ts",
34
+ // "vitest.config.ts",
34
35
 
35
36
  // "package.json",
36
37
  // "tsconfig.json",
37
38
  // "tsconfig.build.json",
38
39
  // "rollup.config.js",
39
- // "vitest.config.ts",
40
40
  // "build-common.js",
41
41
  // "build-umd.js",
42
42
  // "build-esm.js",
43
- // "src/tokenCache.ts",
44
- // "src/generated/runtime.ts",
43
+
45
44
  // "src/quickbaseClient.ts",
46
45
  // "src/invokeMethod.ts",
47
46
  // "src/generated-unified/QuickbaseClient.ts",
48
-
47
+ // "src/authorizationStrategy.ts",
48
+ // "src/rateLimiter.ts",
49
49
  // "src/RateLimitError.ts",
50
- // "src/TokenBucket.ts"
50
+ // "src/ThrottleBucket.ts",
51
+ // "src/tokenCache.ts",
52
+ // "src/utils.ts",
53
+
54
+ // "src/generated/runtime.ts",
55
+ // "src/generated/apis/AuthApi.ts",
56
+ // "src/generated/apis/FieldsApi.ts",
57
+ // "src/generated/apis/AppsApi.ts",
58
+
59
+ "test/vitest/unit/fields/createField.test.ts",
51
60
  ];
52
61
 
53
62
  const projectGoals: string[] = [