quickbase-js 0.1.0-beta.10 → 0.1.0-beta.11

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.
@@ -0,0 +1,68 @@
1
+ // tests/vitest/qb/auth/ssoTokenConcurrency.test.ts
2
+ import { test, expect, vi } from "vitest";
3
+ import { quickbase } from "../../../../src/quickbaseClient";
4
+ import { startMockQuickbaseServer } from "./mockQuickbaseServer";
5
+ import { QB_APP_ID, QB_REALM } from "@tests/setup.ts";
6
+
7
+ test(
8
+ "QuickbaseClient Integration - getApp with SsoTokenStrategy Concurrency",
9
+ async () => {
10
+ const mockServer = startMockQuickbaseServer(3000);
11
+
12
+ // Spy on console.log before any actions
13
+ const consoleSpy = vi.spyOn(console, "log");
14
+
15
+ try {
16
+ const client = quickbase({
17
+ realm: QB_REALM,
18
+ useSso: true,
19
+ samlToken: "mock-saml-token",
20
+ debug: true,
21
+ baseUrl: "http://localhost:3000/v1",
22
+ });
23
+
24
+ const concurrentRequests = [
25
+ client.getApp({ appId: QB_APP_ID }),
26
+ client.getApp({ appId: QB_APP_ID }),
27
+ client.getApp({ appId: QB_APP_ID }),
28
+ ];
29
+
30
+ const results = await Promise.all(concurrentRequests);
31
+
32
+ expect(results).toHaveLength(3);
33
+ results.forEach((result) =>
34
+ expect(result).toEqual({
35
+ id: QB_APP_ID,
36
+ name: "Mock App",
37
+ created: expect.any(Date),
38
+ updated: expect.any(Date),
39
+ })
40
+ );
41
+
42
+ // Verify token fetch and waits using console logs
43
+ const logs = consoleSpy.mock.calls;
44
+ const tokenFetchLogs = logs.filter((call) =>
45
+ call[0].includes("[fetchSsoToken] Fetched token")
46
+ );
47
+ expect(tokenFetchLogs).toHaveLength(1); // Single fetch with synchronization
48
+
49
+ const waitLogs = logs.filter((call) =>
50
+ call[0].includes("Waiting for existing SSO fetch")
51
+ );
52
+ expect(waitLogs.length).toBeGreaterThanOrEqual(2); // At least two waits
53
+
54
+ const appRequestLogs = logs.filter((call) =>
55
+ call[0].includes("Received getApp request")
56
+ );
57
+ expect(appRequestLogs).toHaveLength(3);
58
+
59
+ console.log(
60
+ "SsoTokenStrategy: Single token fetch with concurrent waits confirmed."
61
+ );
62
+ } finally {
63
+ mockServer.close();
64
+ consoleSpy.mockRestore();
65
+ }
66
+ },
67
+ { timeout: 20000 }
68
+ );
@@ -0,0 +1,63 @@
1
+ // tests/vitest/qb/auth/userTokenConcurrency.test.ts
2
+ import { test, expect } from "vitest";
3
+ import { quickbase } from "../../../../src/quickbaseClient";
4
+
5
+ test(
6
+ "QuickbaseClient Integration - getApp with UserToken Concurrency",
7
+ async () => {
8
+ const realm = process.env.QB_REALM || "";
9
+ const appId = process.env.QB_APP_ID || "";
10
+ const userToken = process.env.QB_USER_TOKEN || "";
11
+
12
+ if (!realm || !appId || !userToken) {
13
+ throw new Error(
14
+ "QB_REALM, QB_APP_ID, and QB_USER_TOKEN must be set in .env"
15
+ );
16
+ }
17
+
18
+ const client = quickbase({
19
+ realm,
20
+ userToken,
21
+ debug: true,
22
+ });
23
+
24
+ const concurrentRequests = [
25
+ client.getApp({ appId }),
26
+ client.getApp({ appId }),
27
+ client.getApp({ appId }),
28
+ ];
29
+
30
+ const results = await Promise.all(concurrentRequests);
31
+
32
+ const expectedAppData = {
33
+ id: appId,
34
+ name: "qb-copy",
35
+ created: expect.any(Date),
36
+ updated: expect.any(Date),
37
+ description: "",
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
+ ancestorId: undefined,
51
+ dataClassification: undefined,
52
+ variables: [{ name: "TestVar", value: "TestValue" }],
53
+ };
54
+
55
+ expect(results).toHaveLength(3);
56
+ results.forEach((result) => expect(result).toEqual(expectedAppData));
57
+
58
+ console.log(
59
+ "UserTokenStrategy: All concurrent requests used static token."
60
+ );
61
+ },
62
+ { timeout: 20000 }
63
+ );
@@ -0,0 +1,151 @@
1
+ // tests/vitest/unit/apps/getAppConcurrency.test.ts
2
+
3
+ import { describe, it, expect, beforeEach, vi } from "vitest";
4
+ import { createClient, mockFetch, QB_APP_ID, QB_REALM } from "@tests/setup.ts";
5
+
6
+ describe("QuickbaseClient Unit - getApp Concurrency with Temp Tokens", () => {
7
+ let client: ReturnType<typeof createClient>;
8
+
9
+ beforeEach(() => {
10
+ mockFetch.mockClear();
11
+ // Reset client before each test
12
+ client = createClient(mockFetch, {
13
+ realm: QB_REALM,
14
+ useTempTokens: true, // Use TempTokenStrategy
15
+ debug: true,
16
+ });
17
+ });
18
+
19
+ it("handles concurrent getApp requests with a single token fetch", async () => {
20
+ // Mock the token fetch response
21
+ const tempTokenResponse = {
22
+ temporaryAuthorization: "temp-token-123",
23
+ };
24
+ mockFetch.mockImplementation((url: string, options: RequestInit) => {
25
+ if (url.includes(`/auth/temporary/${QB_APP_ID}`)) {
26
+ return Promise.resolve({
27
+ ok: true,
28
+ status: 200,
29
+ json: () => Promise.resolve(tempTokenResponse),
30
+ });
31
+ }
32
+ // Mock the getApp response
33
+ return Promise.resolve({
34
+ ok: true,
35
+ status: 200,
36
+ json: () =>
37
+ Promise.resolve({
38
+ id: QB_APP_ID,
39
+ name: "test-app",
40
+ created: "2025-02-13T18:22:33Z",
41
+ updated: "2025-03-04T04:25:51Z",
42
+ }),
43
+ });
44
+ });
45
+
46
+ // Simulate concurrent getApp requests
47
+ const concurrentRequests = [
48
+ client.getApp({ appId: QB_APP_ID }),
49
+ client.getApp({ appId: QB_APP_ID }),
50
+ client.getApp({ appId: QB_APP_ID }),
51
+ ];
52
+
53
+ const results = await Promise.all(concurrentRequests);
54
+
55
+ // Assertions
56
+ expect(results).toHaveLength(3); // All requests completed
57
+ results.forEach((result) => {
58
+ expect(result).toEqual({
59
+ id: QB_APP_ID,
60
+ name: "test-app",
61
+ created: expect.any(Date), // Assuming convertDates is true
62
+ updated: expect.any(Date),
63
+ });
64
+ });
65
+
66
+ // Verify token fetch was called exactly once
67
+ const tokenFetchCalls = mockFetch.mock.calls.filter((call) =>
68
+ call[0].includes(`/auth/temporary/${QB_APP_ID}`)
69
+ );
70
+ expect(tokenFetchCalls).toHaveLength(1); // Only one token fetch despite 3 requests
71
+
72
+ // Verify getApp calls were made with the temp token
73
+ const getAppCalls = mockFetch.mock.calls.filter((call) =>
74
+ call[0].includes(`/apps/${QB_APP_ID}`)
75
+ );
76
+ expect(getAppCalls).toHaveLength(3); // Three getApp requests
77
+ getAppCalls.forEach((call) => {
78
+ expect(call[1].headers).toMatchObject({
79
+ Authorization: `QB-TEMP-TOKEN ${tempTokenResponse.temporaryAuthorization}`,
80
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
81
+ "Content-Type": "application/json",
82
+ });
83
+ });
84
+ });
85
+
86
+ it("handles concurrent getApp requests with cached token", async () => {
87
+ // Pre-populate the token cache by making an initial request
88
+ mockFetch.mockResolvedValueOnce({
89
+ ok: true,
90
+ status: 200,
91
+ json: () =>
92
+ Promise.resolve({ temporaryAuthorization: "cached-token-456" }),
93
+ });
94
+ await client.getApp({ appId: QB_APP_ID }); // This caches the token
95
+
96
+ // Reset mock to clear initial call tracking
97
+ mockFetch.mockClear();
98
+
99
+ // Mock only the getApp response now (no token fetch needed)
100
+ mockFetch.mockResolvedValue({
101
+ ok: true,
102
+ status: 200,
103
+ json: () =>
104
+ Promise.resolve({
105
+ id: QB_APP_ID,
106
+ name: "test-app",
107
+ created: "2025-02-13T18:22:33Z",
108
+ updated: "2025-03-04T04:25:51Z",
109
+ }),
110
+ });
111
+
112
+ // Simulate concurrent getApp requests with cached token
113
+ const concurrentRequests = [
114
+ client.getApp({ appId: QB_APP_ID }),
115
+ client.getApp({ appId: QB_APP_ID }),
116
+ client.getApp({ appId: QB_APP_ID }),
117
+ ];
118
+
119
+ const results = await Promise.all(concurrentRequests);
120
+
121
+ // Assertions
122
+ expect(results).toHaveLength(3); // All requests completed
123
+ results.forEach((result) => {
124
+ expect(result).toEqual({
125
+ id: QB_APP_ID,
126
+ name: "test-app",
127
+ created: expect.any(Date),
128
+ updated: expect.any(Date),
129
+ });
130
+ });
131
+
132
+ // Verify no token fetches occurred (cache was used)
133
+ const tokenFetchCalls = mockFetch.mock.calls.filter((call) =>
134
+ call[0].includes(`/auth/temporary/${QB_APP_ID}`)
135
+ );
136
+ expect(tokenFetchCalls).toHaveLength(0); // No token fetches
137
+
138
+ // Verify getApp calls were made with the cached token
139
+ const getAppCalls = mockFetch.mock.calls.filter((call) =>
140
+ call[0].includes(`/apps/${QB_APP_ID}`)
141
+ );
142
+ expect(getAppCalls).toHaveLength(3); // Three getApp requests
143
+ getAppCalls.forEach((call) => {
144
+ expect(call[1].headers).toMatchObject({
145
+ Authorization: `QB-TEMP-TOKEN cached-token-456`,
146
+ "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
147
+ "Content-Type": "application/json",
148
+ });
149
+ });
150
+ });
151
+ });
@@ -42,7 +42,7 @@ describe("QuickbaseClient Unit - 401 with fetchTempToken 401", () => {
42
42
  json: () => Promise.resolve({ message: "Temp token fetch failed" }),
43
43
  });
44
44
  }
45
- return Promise.reject(new Error(`Unexpected fetch call: ${url}`));
45
+ throw new Error(`Unexpected fetch call: ${url}`); // Use throw to stop further retries
46
46
  });
47
47
 
48
48
  const consoleSpy = vi.spyOn(console, "log");
@@ -52,8 +52,26 @@ describe("QuickbaseClient Unit - 401 with fetchTempToken 401", () => {
52
52
  );
53
53
 
54
54
  expect(mockFetch).toHaveBeenCalledTimes(3);
55
+ expect(mockFetch).toHaveBeenNthCalledWith(
56
+ 1,
57
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
58
+ expect.any(Object)
59
+ );
60
+ expect(mockFetch).toHaveBeenNthCalledWith(
61
+ 2,
62
+ `https://api.quickbase.com/v1/fields?tableId=${QB_TABLE_ID_1}`,
63
+ expect.objectContaining({
64
+ headers: expect.objectContaining({
65
+ Authorization: `QB-TEMP-TOKEN ${mockToken}`,
66
+ }),
67
+ })
68
+ );
69
+ expect(mockFetch).toHaveBeenNthCalledWith(
70
+ 3,
71
+ `https://api.quickbase.com/v1/auth/temporary/${QB_TABLE_ID_1}`,
72
+ expect.any(Object)
73
+ );
55
74
 
56
- // Updated to match the single-argument log from handleError
57
75
  expect(consoleSpy).toHaveBeenCalledWith(
58
76
  "Authorization error for getFields (temp token), refreshing token:"
59
77
  );
@@ -0,0 +1,62 @@
1
+ // tests/vitest/unit/auth/userTokenConcurrency.test.ts
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { quickbase } from "../../../../src/quickbaseClient";
4
+ import { QB_APP_ID, QB_REALM, QB_USER_TOKEN, mockFetch } from "@tests/setup.ts";
5
+
6
+ describe("QuickbaseClient Unit - UserTokenStrategy Concurrency", () => {
7
+ it("handles concurrent getApp requests with static user token", async () => {
8
+ mockFetch.mockClear();
9
+ const client = quickbase({
10
+ realm: QB_REALM,
11
+ userToken: QB_USER_TOKEN,
12
+ debug: true,
13
+ fetchApi: mockFetch,
14
+ });
15
+
16
+ mockFetch.mockResolvedValue({
17
+ ok: true,
18
+ status: 200,
19
+ json: () =>
20
+ Promise.resolve({
21
+ id: QB_APP_ID,
22
+ name: "test-app",
23
+ created: "2025-02-13T18:22:33Z",
24
+ updated: "2025-03-22T04:00:18Z",
25
+ }),
26
+ });
27
+
28
+ const concurrentRequests = [
29
+ client.getApp({ appId: QB_APP_ID }),
30
+ client.getApp({ appId: QB_APP_ID }),
31
+ client.getApp({ appId: QB_APP_ID }),
32
+ ];
33
+
34
+ const results = await Promise.all(concurrentRequests);
35
+
36
+ expect(results).toHaveLength(3);
37
+ results.forEach((result) =>
38
+ expect(result).toEqual({
39
+ id: QB_APP_ID,
40
+ name: "test-app",
41
+ created: expect.any(Date),
42
+ updated: expect.any(Date),
43
+ })
44
+ );
45
+
46
+ const appCalls = mockFetch.mock.calls.filter((call) =>
47
+ call[0].includes(`/apps/${QB_APP_ID}`)
48
+ );
49
+ expect(appCalls).toHaveLength(3);
50
+ appCalls.forEach((call) =>
51
+ expect(call[1].headers).toMatchObject({
52
+ Authorization: `QB-USER-TOKEN ${QB_USER_TOKEN}`,
53
+ })
54
+ );
55
+
56
+ // No token fetches occur with UserTokenStrategy
57
+ const tokenFetchCalls = mockFetch.mock.calls.filter((call) =>
58
+ call[0].includes("/auth")
59
+ );
60
+ expect(tokenFetchCalls).toHaveLength(0);
61
+ });
62
+ });
@@ -9,7 +9,7 @@ export * from "./apps/getApp.test.ts";
9
9
  export * from "./auth/fetchTempToken401.test.ts";
10
10
  export * from "./auth/getTempToken.test.ts";
11
11
  export * from "./auth/retryOn401.test.ts";
12
- export * from "./auth/retryTempTokenTwo401.test.ts";
12
+ export * from "./auth/retryTempTokenMaxRetries.test.ts";
13
13
 
14
14
  // Fields
15
15
  export * from "./fields/getFields.test.ts";
@@ -1,13 +1,13 @@
1
1
  // tests/vitest/unit/quickbaseClient.test.ts
2
2
 
3
3
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4
- import { quickbase } from "../../../src/quickbaseClient";
4
+ import { quickbase } from "../../../src/quickbaseClient.ts";
5
5
  import { mockFetch, QB_APP_ID, QB_REALM } from "../../setup.ts";
6
6
  import {
7
7
  UpdateAppRequest,
8
8
  UpdateApp200Response,
9
- } from "../../../src/generated/models";
10
- import { ChangesetSolution200Response } from "../../../src/generated/models";
9
+ } from "../../../src/generated/models/index.ts";
10
+ import { ChangesetSolution200Response } from "../../../src/generated/models/index.ts";
11
11
 
12
12
  describe("QuickbaseClient Unit - HTTP Method Extraction", () => {
13
13
  let clientTemp: ReturnType<typeof quickbase>;
package/tools/log-gen.ts CHANGED
@@ -53,7 +53,7 @@ const includeFiles: string[] = [
53
53
  "src/generated-unified/QuickbaseClient.ts",
54
54
  "src/authorizationStrategy.ts",
55
55
  "src/rateLimiter.ts",
56
- // "src/RateLimitError.ts",
56
+ "src/RateLimitError.ts",
57
57
  "src/ThrottleBucket.ts",
58
58
  "src/tokenCache.ts",
59
59
  "src/utils.ts",