quickbase-js 0.1.0-beta.12 → 0.1.0-beta.13

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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +48 -18
  3. package/dist/esm/quickbase.d.ts +12 -3
  4. package/dist/esm/quickbase.js +416 -49
  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 +416 -49
  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/package.json +3 -1
  13. package/src/BurstAwareThrottleBucket.ts +57 -0
  14. package/src/{ThrottleBucket.ts → FlowThrottleBucket.ts} +5 -35
  15. package/src/RateThrottleBucket.ts +5 -0
  16. package/src/Semaphore.ts +33 -0
  17. package/src/authorizationStrategy.ts +12 -1
  18. package/src/generated-unified/QuickbaseClient.ts +1 -1
  19. package/src/invokeMethod.ts +94 -16
  20. package/src/pagination.ts +429 -0
  21. package/src/quickbaseClient.ts +56 -28
  22. package/src/rateLimiter.ts +3 -3
  23. package/tests/vitest/qb/quickbase-js/BurstAwareThrottleBucket.test.ts +125 -0
  24. package/tests/vitest/qb/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +1 -1
  25. package/tests/vitest/qb/quickbase-js/paginationSkip.test.ts +111 -0
  26. package/tests/vitest/qb/records/runQuery.test.ts +1 -2
  27. package/tests/vitest/unit/auth/retryUserToken401.test.ts +7 -10
  28. package/tests/vitest/unit/quickbase-js/BurstAwareThrottleBucket.test.ts +108 -0
  29. package/tests/vitest/unit/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +5 -5
  30. package/tests/vitest/unit/quickbase-js/pagination.test.ts +251 -0
  31. package/tests/vitest/unit/{quickbaseClientHttpMethods.test.ts → quickbase-js/quickbaseClientHttpMethods.test.ts} +13 -11
  32. package/tests/vitest/unit/records/runQuery.test.ts +3 -4
  33. package/tests/vitest/unit/umd.test.ts +0 -8
  34. /package/tests/vitest/unit/{rateLimit.test.ts → quickbase-js/rateLimit.test.ts} +0 -0
  35. /package/tests/vitest/unit/{tokenLifespan.test.ts → quickbase-js/tokenLifespan.test.ts} +0 -0
@@ -0,0 +1,125 @@
1
+ // tests/vitest/qb/BurstAwareThrottleBucket.test.ts
2
+ import { describe, it, expect } from "vitest";
3
+ import { quickbase } from "../../../../src/quickbaseClient";
4
+
5
+ // Set this to true to run the slow test with 80 burst and 100 calls
6
+ const runSlowTests = false;
7
+
8
+ describe("QuickbaseClient with BurstAwareThrottleBucket Integration Tests", () => {
9
+ // Helper function to reduce duplication
10
+ const runThrottleTest = async (
11
+ burst: number,
12
+ windowSeconds: number,
13
+ callCount: number
14
+ ) => {
15
+ const mockFetch = async (
16
+ url: string,
17
+ options: RequestInit
18
+ ): Promise<Response> => {
19
+ await new Promise((resolve) => setTimeout(resolve, 500));
20
+ return new Response(JSON.stringify({ id: "mock-app" }), {
21
+ status: 200,
22
+ headers: { "Content-Type": "application/json" },
23
+ });
24
+ };
25
+
26
+ const qbClient = quickbase({
27
+ realm: "test",
28
+ userToken: "mock-token",
29
+ throttle: { type: "burst-aware", burst, windowSeconds },
30
+ fetchApi: mockFetch,
31
+ debug: true,
32
+ });
33
+
34
+ const throttleTimes: number[] = [];
35
+ const startTimes: number[] = new Array(callCount).fill(0);
36
+ const completionTimes: number[] = [];
37
+ const recordTime = async (index: number) => {
38
+ const preThrottle = Date.now();
39
+ throttleTimes[index] = preThrottle;
40
+ await qbClient.getApp({
41
+ appId: "mock-app-id",
42
+ startTime: startTimes[index],
43
+ });
44
+ completionTimes[index] = Date.now();
45
+ startTimes[index] = startTimes[index] || Date.now() - 500;
46
+ };
47
+
48
+ const startTime = Date.now();
49
+ const promises = [];
50
+ for (let i = 0; i < callCount; i++) {
51
+ promises.push(recordTime(i));
52
+ await new Promise((resolve) => setTimeout(resolve, 1));
53
+ }
54
+ await Promise.all(promises);
55
+
56
+ const relativeThrottleTimes = throttleTimes.map((t) => t - startTime);
57
+ const relativeStartTimes = startTimes.map((t) => t - startTime);
58
+ const durations = completionTimes.map((t) => t - startTime);
59
+ const timings = completionTimes.map((t, i) => t - startTimes[i]);
60
+
61
+ console.log("Relative Throttle Times:", relativeThrottleTimes.slice(0, 10));
62
+ console.log("Relative Start Times:", relativeStartTimes.slice(0, 10));
63
+ console.log("Durations:", durations.slice(0, 10));
64
+ console.log("Timings:", timings.slice(0, 10));
65
+ console.log("Total Duration:", Date.now() - startTime);
66
+
67
+ return { totalDuration: Date.now() - startTime, durations, timings };
68
+ };
69
+
70
+ it("handles concurrent API calls with burst 3 and 0.5s window", async () => {
71
+ const { totalDuration, durations, timings } = await runThrottleTest(
72
+ 3,
73
+ 0.5,
74
+ 6
75
+ );
76
+
77
+ expect(totalDuration).toBeGreaterThanOrEqual(1000);
78
+ expect(totalDuration).toBeLessThan(1200);
79
+
80
+ expect(durations.length).toBe(6);
81
+ expect(durations.slice(0, 3).every((d) => d >= 500 && d < 550)).toBe(true);
82
+
83
+ const sortedDurations = [...durations].sort((a, b) => a - b);
84
+ expect(sortedDurations[3]).toBeGreaterThanOrEqual(1000);
85
+ expect(sortedDurations[3]).toBeLessThan(1100);
86
+ expect(sortedDurations[4]).toBeGreaterThanOrEqual(1000);
87
+ expect(sortedDurations[4]).toBeLessThan(1100);
88
+ expect(sortedDurations[5]).toBeGreaterThanOrEqual(1000);
89
+ expect(sortedDurations[5]).toBeLessThan(1100);
90
+
91
+ expect(timings.every((t) => t >= 490 && t < 560)).toBe(true);
92
+ });
93
+
94
+ // Optional test for 80 burst - set runSlowTests = true to enable
95
+ if (runSlowTests) {
96
+ it(
97
+ "handles 100 concurrent API calls with burst 80 and 10s window",
98
+ { timeout: 12000 },
99
+ async () => {
100
+ const { totalDuration, durations, timings } = await runThrottleTest(
101
+ 80,
102
+ 10,
103
+ 100
104
+ );
105
+
106
+ expect(totalDuration).toBeGreaterThanOrEqual(10500);
107
+ expect(totalDuration).toBeLessThan(11500);
108
+
109
+ expect(durations.length).toBe(100);
110
+ expect(durations.slice(0, 80).every((d) => d >= 500 && d < 600)).toBe(
111
+ true
112
+ );
113
+
114
+ const sortedDurations = [...durations].sort((a, b) => a - b);
115
+ expect(
116
+ sortedDurations.slice(80).every((d) => d >= 10500 && d < 10600)
117
+ ).toBe(true);
118
+
119
+ expect(timings.every((t) => t >= 490 && t < 560)).toBe(true);
120
+ }
121
+ );
122
+ } else {
123
+ it.skip("handles 100 concurrent API calls with burst 80 and 10s window", () => {});
124
+ }
125
+ });
@@ -1,6 +1,6 @@
1
1
  // tests/vitest/qb/ThrottleBucket.test.ts
2
2
  import { describe, it, expect } from "vitest";
3
- import { quickbase } from "../../../src/quickbaseClient";
3
+ import { quickbase } from "../../../../src/quickbaseClient";
4
4
 
5
5
  describe("QuickbaseClient with ConcurrentThrottleBucket Integration Tests", () => {
6
6
  it("handles concurrent API calls with burst and rate limiting in strict order", async () => {
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect, beforeAll } from "vitest";
2
+ import {
3
+ createClient,
4
+ QB_TABLE_ID_1,
5
+ QB_REALM,
6
+ QB_USER_TOKEN,
7
+ } from "@tests/setup";
8
+ import { QuickbaseClient } from "@/quickbaseClient";
9
+ import {
10
+ RunQueryRequest,
11
+ RunQueryRequestOptions,
12
+ RunQuery200Response,
13
+ } from "@/generated/models";
14
+
15
+ // Extend RunQueryRequestOptions to include sortBy (for reference, not used here)
16
+ interface ExtendedRunQueryRequestOptions extends RunQueryRequestOptions {
17
+ sortBy?: Array<{ fieldId: number; order: "ASC" | "DESC" }>;
18
+ }
19
+
20
+ describe("QuickbaseClient Integration - Pagination with Skip", () => {
21
+ let qb: QuickbaseClient;
22
+
23
+ beforeAll(() => {
24
+ qb = createClient(undefined, {
25
+ realm: QB_REALM,
26
+ userToken: QB_USER_TOKEN,
27
+ debug: true,
28
+ autoPaginate: true, // Enable library's automatic pagination
29
+ });
30
+ console.log("[beforeAll] Client initialized");
31
+ });
32
+
33
+ it(
34
+ "fetches full dataset with auto-pagination and heavy data",
35
+ { timeout: 30000 }, // 30s for multiple calls
36
+ async () => {
37
+ const queryOptions: ExtendedRunQueryRequestOptions = {
38
+ skip: 0, // Start at beginning, no top specified
39
+ // sortBy removed to test without ordering
40
+ };
41
+
42
+ const queryRequest: RunQueryRequest = {
43
+ from: QB_TABLE_ID_1,
44
+ select: [3, 822, 823, 824, 825, 6], // FID 3 for Record ID
45
+ options: queryOptions,
46
+ };
47
+
48
+ const startTime = Date.now();
49
+ console.log(
50
+ "[Test Start] Query request with auto-pagination:",
51
+ queryRequest
52
+ );
53
+
54
+ try {
55
+ const response = (await qb.runQuery({
56
+ body: queryRequest,
57
+ })) as RunQuery200Response & {
58
+ data: Array<{ [key: string]: { value: any } }>;
59
+ metadata: {
60
+ totalRecords: number;
61
+ numRecords: number;
62
+ numFields: number;
63
+ skip: number;
64
+ top?: number;
65
+ };
66
+ };
67
+
68
+ const duration = Date.now() - startTime;
69
+ console.log(`[Test Complete] Response received in ${duration}ms`, {
70
+ recordCount: response.data.length,
71
+ totalRecords: response.metadata.totalRecords,
72
+ defaultTop: response.metadata.top,
73
+ });
74
+
75
+ // Assertions
76
+ expect(response.data).toBeDefined();
77
+ expect(response.data.length).toBeGreaterThan(2429); // More than initial cap, confirming pagination
78
+ expect(response.data.length).toEqual(response.metadata.totalRecords); // Full dataset fetched
79
+ expect(response.metadata).toBeDefined();
80
+ // Removed expect(response.metadata.totalRecords).toBe(5000)
81
+ expect(response.metadata.totalRecords).toBeGreaterThanOrEqual(5000); // Flexible minimum, allows growth
82
+ expect(response.metadata.numFields).toBe(6); // 6 fields with FID 3
83
+ expect(response.metadata.skip).toBe(0); // Initial skip
84
+
85
+ const sampleRecord = response.data[0];
86
+ console.log("[Debug] Sample record:", sampleRecord);
87
+ expect(sampleRecord["6"]?.value).toBeDefined();
88
+ expect(typeof sampleRecord["6"]?.value).toBe("string");
89
+ expect(sampleRecord["6"]?.value.length).toBeGreaterThan(9000); // ~10KB
90
+ expect(sampleRecord["3"]?.value).toBeDefined(); // Check FID 3 exists
91
+ expect(typeof sampleRecord["3"]?.value).toBe("number"); // Record ID is numeric
92
+
93
+ // Check for duplicates using Record ID (FID 3)
94
+ const recordIds = response.data.map(
95
+ (record: { [key: string]: { value: any } }) => record["3"]?.value
96
+ );
97
+ const uniqueRecordIds = new Set(recordIds);
98
+ expect(uniqueRecordIds.size).toEqual(response.data.length); // No duplicates
99
+ expect(uniqueRecordIds.size).toEqual(response.metadata.totalRecords); // All records unique
100
+
101
+ console.log(
102
+ `[Integration Test] Fetched ${response.data.length} records in ${duration}ms, totalRecords: ${response.metadata.totalRecords}`
103
+ );
104
+ } catch (error) {
105
+ const duration = Date.now() - startTime;
106
+ console.error("[Test Error] Failed after", duration, "ms:", error);
107
+ throw error;
108
+ }
109
+ }
110
+ );
111
+ });
@@ -1,4 +1,3 @@
1
- // tests/vitest/qb/records/runQuery.test.ts
2
1
  import { describe, it, expect } from "vitest";
3
2
  import { createClient, QB_TABLE_ID_1 } from "../../../setup.ts";
4
3
  import {
@@ -128,7 +127,7 @@ describe("QuickbaseClient Integration - runQuery", () => {
128
127
  numFields: 2,
129
128
  numRecords: 1,
130
129
  skip: 0,
131
- top: 1,
130
+ // Removed top: 1, as it should be undefined in the final response
132
131
  totalRecords: expect.any(Number),
133
132
  });
134
133
 
@@ -1,5 +1,3 @@
1
- // tests/vitest/unit/auth/retryUserToken401.test.ts
2
-
3
1
  import { describe, it, expect, vi } from "vitest";
4
2
  import {
5
3
  createClient,
@@ -10,7 +8,6 @@ import {
10
8
 
11
9
  describe("QuickbaseClient Unit - User Token Retry on 401", () => {
12
10
  it("retries with the same user token on 401 and succeeds", async () => {
13
- const userToken = "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3";
14
11
  mockFetch
15
12
  .mockResolvedValueOnce(
16
13
  new Response(JSON.stringify({ message: "Unauthorized" }), {
@@ -26,7 +23,7 @@ describe("QuickbaseClient Unit - User Token Retry on 401", () => {
26
23
  const consoleSpy = vi.spyOn(console, "log");
27
24
  const client = createClient(mockFetch, {
28
25
  realm: QB_REALM,
29
- userToken,
26
+ userToken: "b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
30
27
  useTempTokens: false,
31
28
  debug: true,
32
29
  });
@@ -41,7 +38,8 @@ describe("QuickbaseClient Unit - User Token Retry on 401", () => {
41
38
  expect.objectContaining({
42
39
  method: "GET",
43
40
  headers: expect.objectContaining({
44
- Authorization: `QB-USER-TOKEN ${userToken}`,
41
+ Authorization:
42
+ "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
45
43
  "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
46
44
  "Content-Type": "application/json",
47
45
  }),
@@ -53,17 +51,16 @@ describe("QuickbaseClient Unit - User Token Retry on 401", () => {
53
51
  expect.objectContaining({
54
52
  method: "GET",
55
53
  headers: expect.objectContaining({
56
- Authorization: `QB-USER-TOKEN ${userToken}`,
54
+ Authorization:
55
+ "QB-USER-TOKEN b9f3pk_q4jd_0_b4qu5eebyvuix3xs57ysd7zn3",
57
56
  "QB-Realm-Hostname": `${QB_REALM}.quickbase.com`,
58
57
  "Content-Type": "application/json",
59
58
  }),
60
59
  })
61
60
  );
62
61
  expect(consoleSpy).toHaveBeenCalledWith(
63
- `Retrying getFields with existing user token: b9f3pk_q4j...`
64
- );
65
- expect(consoleSpy).toHaveBeenCalledWith(
66
- "[getFields] Retrying with token: b9f3pk_q4j..."
62
+ "Retrying getFields with existing user token: b9f3pk_q4j..."
67
63
  );
64
+ consoleSpy.mockRestore();
68
65
  });
69
66
  });
@@ -0,0 +1,108 @@
1
+ // test/vitest/unit/BurstAwareThrottleBucket.test.ts
2
+ import { describe, it, expect } from "vitest";
3
+ import { BurstAwareThrottleBucket } from "@/BurstAwareThrottleBucket";
4
+
5
+ describe("BurstAwareThrottleBucket Unit Tests", () => {
6
+ it("allows burst capacity immediately", async () => {
7
+ const bucket = new BurstAwareThrottleBucket({
8
+ maxTokens: 3,
9
+ windowSeconds: 10,
10
+ });
11
+ const startTime = Date.now();
12
+
13
+ await Promise.all([
14
+ bucket.acquire().then(() => bucket.release()),
15
+ bucket.acquire().then(() => bucket.release()),
16
+ bucket.acquire().then(() => bucket.release()),
17
+ ]);
18
+
19
+ const duration = Date.now() - startTime;
20
+ expect(duration).toBeLessThan(50);
21
+ });
22
+
23
+ it("delays calls beyond burst until window reset", async () => {
24
+ const bucket = new BurstAwareThrottleBucket({
25
+ maxTokens: 3,
26
+ windowSeconds: 0.5,
27
+ });
28
+ const timings: number[] = [];
29
+ const recordTime = async () => {
30
+ await bucket.acquire();
31
+ timings.push(Date.now());
32
+ bucket.release();
33
+ };
34
+
35
+ const startTime = Date.now();
36
+ await Promise.all([recordTime(), recordTime(), recordTime(), recordTime()]);
37
+
38
+ const durations = timings.map((t) => t - startTime);
39
+ expect(durations.length).toBe(4);
40
+ expect(durations[0]).toBeLessThan(50);
41
+ expect(durations[1]).toBeLessThan(50);
42
+ expect(durations[2]).toBeLessThan(50);
43
+ expect(durations[3]).toBeGreaterThanOrEqual(450);
44
+ expect(durations[3]).toBeLessThan(600); // Relaxed from 550 to 600
45
+ });
46
+
47
+ it("maintains burst limit over sustained calls with window", async () => {
48
+ const bucket = new BurstAwareThrottleBucket({
49
+ maxTokens: 2,
50
+ windowSeconds: 0.5,
51
+ });
52
+ const timings: number[] = [];
53
+ const recordTime = async () => {
54
+ await bucket.acquire();
55
+ timings.push(Date.now());
56
+ bucket.release();
57
+ };
58
+
59
+ const startTime = Date.now();
60
+ await Promise.all([
61
+ recordTime(),
62
+ recordTime(),
63
+ recordTime(),
64
+ recordTime(),
65
+ recordTime(),
66
+ ]);
67
+
68
+ const durations = timings.map((t) => t - startTime);
69
+ expect(durations.length).toBe(5);
70
+ expect(durations[0]).toBeLessThan(50);
71
+ expect(durations[1]).toBeLessThan(50);
72
+ expect(durations[2]).toBeGreaterThanOrEqual(450);
73
+ expect(durations[2]).toBeLessThan(600); // Relaxed from 550 to 600
74
+ expect(durations[3]).toBeGreaterThanOrEqual(450);
75
+ expect(durations[3]).toBeLessThan(600); // Relaxed from 550 to 600
76
+ expect(durations[4]).toBeGreaterThanOrEqual(950);
77
+ expect(durations[4]).toBeLessThan(1100); // Relaxed from 1050 to 1100
78
+ });
79
+
80
+ it("handles high concurrency without exceeding burst within window", async () => {
81
+ const bucket = new BurstAwareThrottleBucket({
82
+ maxTokens: 3,
83
+ windowSeconds: 0.5,
84
+ });
85
+ const timings: number[] = [];
86
+ const recordTime = async () => {
87
+ await bucket.acquire();
88
+ timings.push(Date.now());
89
+ bucket.release();
90
+ };
91
+
92
+ const startTime = Date.now();
93
+ await Promise.all(
94
+ Array(10)
95
+ .fill(null)
96
+ .map(() => recordTime())
97
+ );
98
+
99
+ const durations = timings.map((t) => t - startTime);
100
+ expect(durations.length).toBe(10);
101
+ expect(durations.slice(0, 3).every((d) => d < 50)).toBe(true);
102
+ for (let i = 3; i < 10; i++) {
103
+ const expectedDelay = Math.floor((i - 3) / 3) * 500 + 450;
104
+ expect(durations[i]).toBeGreaterThanOrEqual(expectedDelay - 50);
105
+ expect(durations[i]).toBeLessThan(expectedDelay + 100); // Relaxed from 50 to 100
106
+ }
107
+ });
108
+ });
@@ -1,10 +1,10 @@
1
1
  // test/vitest/unit/ThrottleBucket.test.ts
2
2
  import { describe, it, expect, vi } from "vitest";
3
- import { ConcurrentThrottleBucket } from "@/ThrottleBucket"; // Should work with your alias
3
+ import { FlowThrottleBucket } from "@/FlowThrottleBucket"; // Should work with your alias
4
4
 
5
5
  describe("ConcurrentThrottleBucket Unit Tests", () => {
6
6
  it("allows burst capacity immediately", async () => {
7
- const bucket = new ConcurrentThrottleBucket(5, 3); // rate: 5, burst: 3
7
+ const bucket = new FlowThrottleBucket(5, 3); // rate: 5, burst: 3
8
8
  const startTime = Date.now();
9
9
 
10
10
  await Promise.all([
@@ -18,7 +18,7 @@ describe("ConcurrentThrottleBucket Unit Tests", () => {
18
18
  });
19
19
 
20
20
  it("delays calls beyond burst at correct rate", async () => {
21
- const bucket = new ConcurrentThrottleBucket(5, 3); // rate: 5 tokens/sec, burst: 3
21
+ const bucket = new FlowThrottleBucket(5, 3); // rate: 5 tokens/sec, burst: 3
22
22
  const timings: number[] = [];
23
23
  const recordTime = async () => {
24
24
  await bucket.acquire();
@@ -44,7 +44,7 @@ describe("ConcurrentThrottleBucket Unit Tests", () => {
44
44
  });
45
45
 
46
46
  it("maintains rate over sustained calls", async () => {
47
- const bucket = new ConcurrentThrottleBucket(2, 2); // rate: 2 tokens/sec, burst: 2
47
+ const bucket = new FlowThrottleBucket(2, 2); // rate: 2 tokens/sec, burst: 2
48
48
  const timings: number[] = [];
49
49
  const recordTime = async () => {
50
50
  await bucket.acquire();
@@ -74,7 +74,7 @@ describe("ConcurrentThrottleBucket Unit Tests", () => {
74
74
  });
75
75
 
76
76
  it("handles high concurrency without exceeding burst", async () => {
77
- const bucket = new ConcurrentThrottleBucket(5, 3); // rate: 5, burst: 3
77
+ const bucket = new FlowThrottleBucket(5, 3); // rate: 5, burst: 3
78
78
  const timings: number[] = [];
79
79
  const recordTime = async () => {
80
80
  await bucket.acquire();