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,251 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { paginateRecords, isPaginatable } from "@/pagination";
3
+ import { QuickbaseClient } from "@/quickbaseClient";
4
+ import * as invokeMethodModule from "@/invokeMethod";
5
+ import { GetUsers200Response, GetUsersRequest } from "@/generated/models";
6
+
7
+ // Extend GetUsersRequest to include nextPageToken
8
+ interface TestGetUsersRequest extends GetUsersRequest {
9
+ nextPageToken?: string;
10
+ }
11
+
12
+ describe("Pagination Unit Tests", () => {
13
+ const methodMap = {
14
+ runQuery: {
15
+ api: {},
16
+ method: vi.fn(),
17
+ paramMap: ["body"],
18
+ httpMethod: "POST",
19
+ },
20
+ getUsers: {
21
+ api: {},
22
+ method: vi.fn(),
23
+ paramMap: ["body"],
24
+ httpMethod: "POST",
25
+ },
26
+ } as any;
27
+ const baseHeaders = {};
28
+ const authStrategy = {
29
+ getToken: vi.fn(),
30
+ applyHeaders: vi.fn(),
31
+ handleError: vi.fn(),
32
+ } as any;
33
+ const rateLimiter = {
34
+ throttle: vi.fn(),
35
+ release: vi.fn(),
36
+ maxRetries: 3,
37
+ } as any;
38
+ const transformDates = (obj: any) => obj;
39
+
40
+ it("fetches all pages for runQuery with skip-based pagination", async () => {
41
+ const mockInvoke = vi
42
+ .spyOn(invokeMethodModule, "invokeMethod")
43
+ .mockResolvedValueOnce({
44
+ data: Array(2429).fill({ "3": { value: "record" } }),
45
+ metadata: {
46
+ totalRecords: 5000,
47
+ numRecords: 2429,
48
+ numFields: 6,
49
+ skip: 0,
50
+ },
51
+ })
52
+ .mockResolvedValueOnce({
53
+ data: Array(2429).fill({ "3": { value: "record" } }),
54
+ metadata: {
55
+ totalRecords: 5000,
56
+ numRecords: 2429,
57
+ numFields: 6,
58
+ skip: 2429,
59
+ },
60
+ })
61
+ .mockResolvedValueOnce({
62
+ data: Array(142).fill({ "3": { value: "record" } }),
63
+ metadata: {
64
+ totalRecords: 5000,
65
+ numRecords: 142,
66
+ numFields: 6,
67
+ skip: 4858,
68
+ },
69
+ });
70
+
71
+ const result = await paginateRecords(
72
+ "runQuery",
73
+ { body: { from: "mock-table", select: [3, 6] } },
74
+ methodMap,
75
+ baseHeaders,
76
+ authStrategy,
77
+ rateLimiter,
78
+ transformDates,
79
+ false,
80
+ true
81
+ );
82
+
83
+ expect(mockInvoke).toHaveBeenCalledTimes(3);
84
+ expect(result.data.length).toBe(5000);
85
+ expect(result.metadata).toEqual({
86
+ totalRecords: 5000,
87
+ numRecords: 5000,
88
+ numFields: 6,
89
+ skip: 0,
90
+ top: undefined,
91
+ });
92
+ });
93
+
94
+ it("fetches all pages for getUsers with token-based pagination", async () => {
95
+ const mockInvoke = vi
96
+ .spyOn(invokeMethodModule, "invokeMethod")
97
+ .mockImplementation(
98
+ async <K extends keyof QuickbaseClient>(
99
+ methodName: K,
100
+ params: Parameters<QuickbaseClient[K]>[0] & {
101
+ skip?: number;
102
+ top?: number;
103
+ },
104
+ methodMap: any,
105
+ baseHeaders: any,
106
+ authStrategy: any,
107
+ rateLimiter: any,
108
+ transformDates: any,
109
+ debug: any,
110
+ convertDates: any,
111
+ autoPaginate: any,
112
+ attempt: any,
113
+ maxAttempts: any,
114
+ isPaginating: any
115
+ ): Promise<ReturnType<QuickbaseClient[K]>> => {
116
+ if (methodName !== "getUsers")
117
+ throw new Error("Mock only supports 'getUsers'");
118
+ const body = (params as { body?: TestGetUsersRequest }).body;
119
+ const token = body?.nextPageToken;
120
+ if (!token) {
121
+ return {
122
+ users: Array(10).fill({ id: "user1" }),
123
+ metadata: { nextPageToken: "token1" },
124
+ } as GetUsers200Response as ReturnType<QuickbaseClient[K]>;
125
+ } else if (token === "token1") {
126
+ return {
127
+ users: Array(5).fill({ id: "user2" }),
128
+ metadata: { nextPageToken: "token2" },
129
+ } as GetUsers200Response as ReturnType<QuickbaseClient[K]>;
130
+ } else if (token === "token2") {
131
+ return {
132
+ users: Array(3).fill({ id: "user3" }),
133
+ metadata: { nextPageToken: "" },
134
+ } as GetUsers200Response as ReturnType<QuickbaseClient[K]>;
135
+ }
136
+ throw new Error("Unexpected token");
137
+ }
138
+ );
139
+
140
+ const result = await paginateRecords(
141
+ "getUsers",
142
+ { body: { userIds: ["id1", "id2"] } },
143
+ methodMap,
144
+ baseHeaders,
145
+ authStrategy,
146
+ rateLimiter,
147
+ transformDates,
148
+ false,
149
+ true
150
+ );
151
+
152
+ expect(mockInvoke).toHaveBeenCalledTimes(3);
153
+ expect(mockInvoke).toHaveBeenNthCalledWith(
154
+ 1,
155
+ "getUsers",
156
+ expect.objectContaining({ body: { userIds: ["id1", "id2"] } }),
157
+ methodMap,
158
+ baseHeaders,
159
+ authStrategy,
160
+ rateLimiter,
161
+ transformDates,
162
+ false,
163
+ true,
164
+ false,
165
+ 0,
166
+ 4,
167
+ true
168
+ );
169
+ expect(mockInvoke).toHaveBeenNthCalledWith(
170
+ 2,
171
+ "getUsers",
172
+ expect.objectContaining({
173
+ body: { userIds: ["id1", "id2"], nextPageToken: "token1" },
174
+ }),
175
+ methodMap,
176
+ baseHeaders,
177
+ authStrategy,
178
+ rateLimiter,
179
+ transformDates,
180
+ false,
181
+ true,
182
+ false,
183
+ 0,
184
+ 4,
185
+ true
186
+ );
187
+ expect(mockInvoke).toHaveBeenNthCalledWith(
188
+ 3,
189
+ "getUsers",
190
+ expect.objectContaining({
191
+ body: { userIds: ["id1", "id2"], nextPageToken: "token2" },
192
+ }),
193
+ methodMap,
194
+ baseHeaders,
195
+ authStrategy,
196
+ rateLimiter,
197
+ transformDates,
198
+ false,
199
+ true,
200
+ false,
201
+ 0,
202
+ 4,
203
+ true
204
+ );
205
+ expect(result.users.length).toBe(18);
206
+ expect(result.metadata).toEqual({
207
+ numRecords: 18,
208
+ numFields: undefined,
209
+ nextPageToken: "",
210
+ });
211
+ });
212
+
213
+ it("stops when all skip-based records are fetched in one page", async () => {
214
+ const mockInvoke = vi
215
+ .spyOn(invokeMethodModule, "invokeMethod")
216
+ .mockResolvedValueOnce({
217
+ data: Array(10).fill({ id: "record" }),
218
+ metadata: { totalRecords: 10, numRecords: 10, numFields: 3, skip: 0 },
219
+ });
220
+
221
+ const result = await paginateRecords(
222
+ "runQuery",
223
+ { body: { from: "mock-table", select: [3] } },
224
+ methodMap,
225
+ baseHeaders,
226
+ authStrategy,
227
+ rateLimiter,
228
+ transformDates,
229
+ false,
230
+ true
231
+ );
232
+
233
+ expect(mockInvoke).toHaveBeenCalledTimes(1);
234
+ expect(result.data.length).toBe(10);
235
+ expect(result.metadata.totalRecords).toBe(10);
236
+ expect(result.metadata.numRecords).toBe(10);
237
+ });
238
+
239
+ it("isPaginatable detects metadata correctly", () => {
240
+ const skipResponse = {
241
+ data: [],
242
+ metadata: { totalRecords: 100, numRecords: 25, numFields: 3, skip: 0 },
243
+ };
244
+ const tokenResponse = { users: [], metadata: { nextPageToken: "abc" } };
245
+ const invalidResponse = { data: [], other: "stuff" };
246
+
247
+ expect(isPaginatable(skipResponse)).toBe(true);
248
+ expect(isPaginatable(tokenResponse)).toBe(true);
249
+ expect(isPaginatable(invalidResponse)).toBe(false);
250
+ });
251
+ });
@@ -1,13 +1,11 @@
1
- // tests/vitest/unit/quickbaseClient.test.ts
2
-
3
1
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4
- import { quickbase } from "../../../src/quickbaseClient.ts";
5
- import { mockFetch, QB_APP_ID, QB_REALM } from "../../setup.ts";
2
+ import { quickbase } from "@/quickbaseClient";
3
+ import { mockFetch, QB_APP_ID, QB_REALM } from "../../../setup.ts";
6
4
  import {
7
5
  UpdateAppRequest,
8
6
  UpdateApp200Response,
9
- } from "../../../src/generated/models/index.ts";
10
- import { ChangesetSolution200Response } from "../../../src/generated/models/index.ts";
7
+ } from "../../../../src/generated/models/index.ts";
8
+ import { ChangesetSolutionFromRecord200Response } from "../../../../src/generated/models/index.ts";
11
9
 
12
10
  describe("QuickbaseClient Unit - HTTP Method Extraction", () => {
13
11
  let clientTemp: ReturnType<typeof quickbase>;
@@ -58,7 +56,9 @@ describe("QuickbaseClient Unit - HTTP Method Extraction", () => {
58
56
  dataClassification: "None",
59
57
  };
60
58
 
61
- const mockChangesetResponse: ChangesetSolution200Response = [{ changes: [] }];
59
+ const mockChangesetResponse: ChangesetSolutionFromRecord200Response = [
60
+ { changes: [] },
61
+ ];
62
62
 
63
63
  const setupMockFetch = () => {
64
64
  mockFetch.mockReset();
@@ -109,14 +109,16 @@ describe("QuickbaseClient Unit - HTTP Method Extraction", () => {
109
109
  expect(apiCall.method).toBe("POST");
110
110
  });
111
111
 
112
- it("uses PUT for changesetSolution", async () => {
112
+ it("uses GET for changesetSolutionFromRecord", async () => {
113
113
  setupMockFetchForChangeset();
114
- await clientUser.changesetSolution({
114
+ await clientUser.changesetSolutionFromRecord({
115
115
  solutionId: "solution1",
116
- body: "name: Test Solution\nversion: 1.0",
116
+ tableId: "table1",
117
+ fieldId: 6,
118
+ recordId: "123",
117
119
  });
118
120
  const apiCall = mockFetch.mock.calls[0][1];
119
- expect(apiCall.method).toBe("PUT");
121
+ expect(apiCall.method).toBe("GET");
120
122
  });
121
123
 
122
124
  it("uses GET for getTable", async () => {
@@ -1,4 +1,3 @@
1
- // tests/vitest/unit/records/runQuery.test.ts
2
1
  import { describe, test, expect, beforeEach } from "vitest";
3
2
  import {
4
3
  createClient,
@@ -7,7 +6,7 @@ import {
7
6
  QB_USER_TOKEN,
8
7
  QB_TABLE_ID_1,
9
8
  } from "@tests/setup.ts";
10
- import { RunQueryRequest, RunQueryResponse } from "@/generated";
9
+ import { RunQueryRequest, RunQuery200Response } from "@/generated";
11
10
 
12
11
  describe("QuickbaseClient Unit - runQuery", () => {
13
12
  let client: ReturnType<typeof createClient>;
@@ -27,7 +26,7 @@ describe("QuickbaseClient Unit - runQuery", () => {
27
26
  options: { skip: 0, top: 100, compareWithAppLocalTime: false },
28
27
  };
29
28
 
30
- const mockResponse: RunQueryResponse = {
29
+ const mockResponse: RunQuery200Response = {
31
30
  data: [
32
31
  { "3": { value: 1 }, "6": { value: "Task 1" }, "7": { value: "High" } },
33
32
  ],
@@ -40,7 +39,7 @@ describe("QuickbaseClient Unit - runQuery", () => {
40
39
  numFields: 3,
41
40
  numRecords: 1,
42
41
  skip: 0,
43
- top: 100,
42
+ // top is omitted or undefined in the final response unless pagination continues
44
43
  totalRecords: 1,
45
44
  },
46
45
  };
@@ -1,8 +0,0 @@
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
- });