quickbase-js 0.1.0-beta.11 → 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 (37) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +40 -18
  3. package/dist/esm/quickbase.d.ts +12 -3
  4. package/dist/esm/quickbase.js +452 -43
  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 +452 -43
  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/FlowThrottleBucket.ts +47 -0
  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 +102 -17
  20. package/src/pagination.ts +429 -0
  21. package/src/quickbaseClient.ts +56 -28
  22. package/src/rateLimiter.ts +9 -4
  23. package/tests/vitest/qb/fields/getFieldsUsage.test.ts +2 -2
  24. package/tests/vitest/qb/quickbase-js/BurstAwareThrottleBucket.test.ts +125 -0
  25. package/tests/vitest/qb/quickbase-js/FlowBucket.test.ts +77 -0
  26. package/tests/vitest/qb/quickbase-js/paginationSkip.test.ts +111 -0
  27. package/tests/vitest/qb/records/runQuery.test.ts +1 -2
  28. package/tests/vitest/unit/auth/retryUserToken401.test.ts +7 -10
  29. package/tests/vitest/unit/quickbase-js/BurstAwareThrottleBucket.test.ts +108 -0
  30. package/tests/vitest/unit/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +16 -10
  31. package/tests/vitest/unit/quickbase-js/pagination.test.ts +251 -0
  32. package/tests/vitest/unit/{quickbaseClientHttpMethods.test.ts → quickbase-js/quickbaseClientHttpMethods.test.ts} +13 -11
  33. package/tests/vitest/unit/records/runQuery.test.ts +3 -4
  34. package/src/ThrottleBucket.ts +0 -41
  35. package/tests/vitest/unit/umd.test.ts +0 -8
  36. /package/tests/vitest/unit/{rateLimit.test.ts → quickbase-js/rateLimit.test.ts} +0 -0
  37. /package/tests/vitest/unit/{tokenLifespan.test.ts → quickbase-js/tokenLifespan.test.ts} +0 -0
@@ -0,0 +1,429 @@
1
+ import { QuickbaseClient } from "./quickbaseClient";
2
+ import { invokeMethod, MethodInfo } from "./invokeMethod";
3
+ import { AuthorizationStrategy } from "./authorizationStrategy";
4
+ import { RateLimiter } from "./rateLimiter";
5
+
6
+ /**
7
+ * Represents a paginated response from the Quick Base API.
8
+ * @template T - The type of items in the data array.
9
+ */
10
+ interface PaginatedResponse<T> {
11
+ /** The key containing the array of records (e.g., "data" or "users") and additional metadata. */
12
+ [key: string]: T[] | any;
13
+ /** Optional array of field definitions for the records. */
14
+ fields?: { id: number; label: string; type: string }[];
15
+ /** Metadata describing the pagination state. */
16
+ metadata: {
17
+ /** Total number of records available (skip-based pagination). */
18
+ totalRecords?: number;
19
+ /** Number of records in the current page. */
20
+ numRecords?: number;
21
+ /** Number of fields in the records. */
22
+ numFields?: number;
23
+ /** Number of records skipped in the current page (skip-based pagination). */
24
+ skip?: number;
25
+ /** Maximum number of records per page (skip-based pagination). */
26
+ top?: number;
27
+ /** Token for the next page (token-based pagination). */
28
+ nextPageToken?: string;
29
+ /** Alternative token for the next page (token-based pagination). */
30
+ nextToken?: string;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Represents a request body that may include pagination options.
36
+ */
37
+ interface BodyWithOptions {
38
+ /** Optional request body containing pagination parameters. */
39
+ body?: {
40
+ /** Optional pagination options for skip-based requests. */
41
+ options?: {
42
+ /** Sorting criteria for the records. */
43
+ sortBy?: Array<{ fieldId: number; order: "ASC" | "DESC" }>;
44
+ /** Number of records to skip. */
45
+ skip?: number;
46
+ /** Maximum number of records to return per page. */
47
+ top?: number;
48
+ /** Token for the next page (token-based pagination). */
49
+ nextPageToken?: string;
50
+ /** Alternative token for the next page (token-based pagination). */
51
+ nextToken?: string;
52
+ /** Additional arbitrary options. */
53
+ [key: string]: any;
54
+ };
55
+ /** Token for the next page (token-based pagination, directly in body). */
56
+ nextPageToken?: string;
57
+ /** Alternative token for the next page (token-based pagination, directly in body). */
58
+ nextToken?: string;
59
+ /** Additional arbitrary body properties. */
60
+ [key: string]: any;
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Defines the continuation state for pagination, either skip-based or token-based.
66
+ */
67
+ type PaginationContinuation =
68
+ | { type: "skip"; value: number } // Skip-based: number of records to skip next
69
+ | { type: "token"; value: string; key: "nextPageToken" | "nextToken" }; // Token-based: token for the next page
70
+
71
+ /**
72
+ * Paginates records from a Quick Base API endpoint, handling both skip-based and token-based pagination.
73
+ * @template K - The method name keyof QuickbaseClient.
74
+ * @param methodName - The Quick Base API method to invoke (e.g., "runQuery", "getUsers").
75
+ * @param params - Parameters for the API method, including optional skip and top values.
76
+ * @param methodMap - Mapping of method names to their implementation details.
77
+ * @param baseHeaders - Base HTTP headers for API requests.
78
+ * @param authStrategy - Strategy for handling authentication tokens.
79
+ * @param rateLimiter - Rate limiting mechanism to respect API limits.
80
+ * @param transformDates - Function to transform date strings in the response.
81
+ * @param debug - If true, logs detailed pagination information to the console.
82
+ * @param convertDates - If true, converts date strings to Date objects in the response.
83
+ * @param initialResponse - Optional initial response to start pagination from (used by invokeMethod).
84
+ * @returns A promise resolving to the complete paginated response with all records.
85
+ * @throws {Error} If no response is received during pagination.
86
+ */
87
+ export async function paginateRecords<K extends keyof QuickbaseClient>(
88
+ methodName: K,
89
+ params: Parameters<QuickbaseClient[K]>[0] & { skip?: number; top?: number },
90
+ methodMap: { [P in keyof QuickbaseClient]: MethodInfo<P> },
91
+ baseHeaders: Record<string, string>,
92
+ authStrategy: AuthorizationStrategy,
93
+ rateLimiter: RateLimiter,
94
+ transformDates: (obj: any, convertStringsToDates: boolean) => any,
95
+ debug: boolean | undefined,
96
+ convertDates: boolean,
97
+ initialResponse?: PaginatedResponse<any>
98
+ ): Promise<ReturnType<QuickbaseClient[K]>> {
99
+ if (debug) console.log("[paginateRecords] Starting with params:", params);
100
+
101
+ /** Extracts the key containing the data array from the response (e.g., "data" or "users"). */
102
+ const getDataKey = (response: PaginatedResponse<any>): string =>
103
+ Object.keys(response).find((key) => Array.isArray(response[key])) || "data";
104
+
105
+ const dataKey = initialResponse ? getDataKey(initialResponse) : "data";
106
+ let allRecords: any[] = initialResponse ? initialResponse[dataKey] : [];
107
+ let lastResponse: PaginatedResponse<any> | undefined = initialResponse;
108
+ let requestCount = 0;
109
+
110
+ /**
111
+ * Determines the pagination strategy based on response metadata.
112
+ * @param response - The API response to analyze.
113
+ * @returns "skip" for skip-based, "token" for token-based, or "none" if no pagination is detected.
114
+ */
115
+ const determinePaginationType = (
116
+ response: PaginatedResponse<any>
117
+ ): "skip" | "token" | "none" => {
118
+ if (
119
+ response.metadata.totalRecords !== undefined &&
120
+ response.metadata.skip !== undefined
121
+ ) {
122
+ if (debug)
123
+ console.log("[paginateRecords] Detected skip-based pagination");
124
+ return "skip";
125
+ }
126
+ if (
127
+ response.metadata.nextPageToken !== undefined ||
128
+ response.metadata.nextToken !== undefined
129
+ ) {
130
+ if (debug)
131
+ console.log("[paginateRecords] Detected token-based pagination");
132
+ return "token";
133
+ }
134
+ if (debug)
135
+ console.log(
136
+ "[paginateRecords] No clear pagination type detected, assuming single-page response"
137
+ );
138
+ return "none"; // No pagination indicators
139
+ };
140
+
141
+ let paginationType: "skip" | "token" | "none" = initialResponse
142
+ ? determinePaginationType(initialResponse)
143
+ : "none"; // Start with no pagination assumption
144
+
145
+ let continuation: PaginationContinuation | null = initialResponse
146
+ ? paginationType === "skip"
147
+ ? {
148
+ type: "skip",
149
+ value:
150
+ (initialResponse.metadata.skip ?? 0) +
151
+ (initialResponse.metadata.numRecords || allRecords.length),
152
+ }
153
+ : paginationType === "token"
154
+ ? {
155
+ type: "token",
156
+ value:
157
+ initialResponse.metadata.nextPageToken ||
158
+ initialResponse.metadata.nextToken ||
159
+ "",
160
+ key:
161
+ initialResponse.metadata.nextPageToken !== undefined
162
+ ? "nextPageToken"
163
+ : "nextToken",
164
+ }
165
+ : null
166
+ : null; // No continuation until type is determined
167
+
168
+ if (debug) {
169
+ console.log("[paginateRecords] Initial state:", {
170
+ dataKey,
171
+ paginationType,
172
+ continuation,
173
+ hasInitialResponse: !!initialResponse,
174
+ totalRecordsSoFar: allRecords.length,
175
+ });
176
+ if (initialResponse && initialResponse[dataKey].length > 0) {
177
+ console.log(
178
+ "[paginateRecords] Initial response IDs: first:",
179
+ initialResponse[dataKey][0]["3"]?.value,
180
+ "last:",
181
+ initialResponse[dataKey][initialResponse[dataKey].length - 1]["3"]
182
+ ?.value
183
+ );
184
+ }
185
+ }
186
+
187
+ /** Fetches the next page of records using the provided parameters. */
188
+ const fetchNextPage = async (
189
+ paginatedParams: any
190
+ ): Promise<PaginatedResponse<any>> => {
191
+ if (debug)
192
+ console.log("[paginateRecords] Fetching with params:", paginatedParams);
193
+ const response = await invokeMethod(
194
+ methodName,
195
+ paginatedParams,
196
+ methodMap,
197
+ baseHeaders,
198
+ authStrategy,
199
+ rateLimiter,
200
+ transformDates,
201
+ debug,
202
+ convertDates,
203
+ false,
204
+ 0,
205
+ rateLimiter.maxRetries + 1,
206
+ true
207
+ );
208
+ requestCount++;
209
+ return response as PaginatedResponse<any>;
210
+ };
211
+
212
+ /** Constructs the parameters for the next API request based on the pagination type. */
213
+ const getPaginatedParams = (): any => {
214
+ const hasBody = "body" in params && params.body !== undefined;
215
+ if (paginationType === "skip" && continuation?.type === "skip") {
216
+ const paginatedOptions = {
217
+ ...(hasBody && "options" in params.body! ? params.body!.options : {}),
218
+ skip: continuation.value,
219
+ };
220
+ return hasBody
221
+ ? {
222
+ ...params,
223
+ body: {
224
+ ...(params as BodyWithOptions).body!,
225
+ options: paginatedOptions,
226
+ },
227
+ }
228
+ : { ...params, skip: continuation.value };
229
+ } else if (paginationType === "token" && continuation?.type === "token") {
230
+ if (continuation.value === "" && !lastResponse) {
231
+ return { ...params }; // Initial call, no pagination fields
232
+ }
233
+ const tokenKey = continuation.key;
234
+ return hasBody
235
+ ? {
236
+ ...params,
237
+ body: {
238
+ ...(params as BodyWithOptions).body!,
239
+ [tokenKey]: continuation.value,
240
+ },
241
+ }
242
+ : { ...params, [tokenKey]: continuation.value };
243
+ }
244
+ return params; // No pagination or initial call
245
+ };
246
+
247
+ if (!initialResponse) {
248
+ const firstParams = getPaginatedParams();
249
+ const firstResponse = await fetchNextPage(firstParams);
250
+ const actualDataKey = getDataKey(firstResponse);
251
+ allRecords = firstResponse[actualDataKey];
252
+ lastResponse = firstResponse;
253
+ paginationType = determinePaginationType(firstResponse);
254
+ continuation =
255
+ paginationType === "skip"
256
+ ? {
257
+ type: "skip",
258
+ value:
259
+ (firstResponse.metadata.skip ?? 0) +
260
+ (firstResponse.metadata.numRecords || allRecords.length),
261
+ }
262
+ : paginationType === "token"
263
+ ? {
264
+ type: "token",
265
+ value:
266
+ firstResponse.metadata.nextPageToken ??
267
+ firstResponse.metadata.nextToken ??
268
+ "",
269
+ key:
270
+ firstResponse.metadata.nextPageToken !== undefined
271
+ ? "nextPageToken"
272
+ : "nextToken",
273
+ }
274
+ : null;
275
+ }
276
+
277
+ while (true) {
278
+ const hasMore =
279
+ (paginationType === "skip" &&
280
+ continuation?.type === "skip" &&
281
+ lastResponse!.metadata.totalRecords !== undefined &&
282
+ continuation.value < lastResponse!.metadata.totalRecords &&
283
+ allRecords.length < lastResponse!.metadata.totalRecords) ||
284
+ (paginationType === "token" &&
285
+ continuation?.type === "token" &&
286
+ continuation.value !== "");
287
+
288
+ if (!hasMore) {
289
+ if (debug) {
290
+ console.log(
291
+ "[paginateRecords] Stopping:",
292
+ paginationType === "skip"
293
+ ? "All skip-based records fetched or no more data"
294
+ : paginationType === "token"
295
+ ? "Token exhausted"
296
+ : "No pagination required"
297
+ );
298
+ }
299
+ break;
300
+ }
301
+
302
+ const paginatedParams = getPaginatedParams();
303
+ const response = await fetchNextPage(paginatedParams);
304
+ const actualDataKey = getDataKey(response);
305
+ allRecords = allRecords.concat(response[actualDataKey]);
306
+ lastResponse = response;
307
+
308
+ if (debug && response[actualDataKey].length > 0) {
309
+ console.log(
310
+ "[paginateRecords] Fetched records this iteration:",
311
+ response[actualDataKey].length,
312
+ "IDs: first:",
313
+ response[actualDataKey][0]["3"]?.value,
314
+ "last:",
315
+ response[actualDataKey][response[actualDataKey].length - 1]["3"]?.value,
316
+ "Continuation after fetch:",
317
+ continuation
318
+ );
319
+ }
320
+
321
+ if (paginationType === "skip") {
322
+ const newSkip =
323
+ continuation?.type === "skip"
324
+ ? continuation.value + response[actualDataKey].length
325
+ : (response.metadata.skip ?? 0) + response[actualDataKey].length;
326
+ continuation = { type: "skip", value: newSkip };
327
+ if (response[actualDataKey].length === 0) {
328
+ if (
329
+ debug &&
330
+ lastResponse!.metadata.totalRecords &&
331
+ newSkip < lastResponse!.metadata.totalRecords
332
+ ) {
333
+ console.warn(
334
+ "[paginateRecords] Warning: Empty response received but records remain",
335
+ { skip: newSkip, totalRecords: lastResponse!.metadata.totalRecords }
336
+ );
337
+ }
338
+ break;
339
+ }
340
+ } else if (paginationType === "token") {
341
+ const newToken =
342
+ response.metadata.nextPageToken ?? response.metadata.nextToken ?? "";
343
+ continuation = {
344
+ type: "token",
345
+ value: newToken,
346
+ key:
347
+ response.metadata.nextPageToken !== undefined
348
+ ? "nextPageToken"
349
+ : "nextToken",
350
+ };
351
+ if (continuation.value === "") {
352
+ break;
353
+ } else if (response[actualDataKey].length === 0 && newToken !== "") {
354
+ if (debug)
355
+ console.warn(
356
+ "[paginateRecords] Warning: Empty response with non-empty token, possible API inconsistency",
357
+ { token: newToken }
358
+ );
359
+ }
360
+ }
361
+ }
362
+
363
+ if (!lastResponse) {
364
+ throw new Error("[paginateRecords] No response received during pagination");
365
+ }
366
+
367
+ const finalDataKey = getDataKey(lastResponse);
368
+ const finalResponse: PaginatedResponse<any> = {
369
+ [finalDataKey]: allRecords,
370
+ fields: lastResponse.fields,
371
+ metadata: {
372
+ totalRecords:
373
+ paginationType === "skip"
374
+ ? lastResponse.metadata.totalRecords
375
+ : undefined,
376
+ numRecords: allRecords.length,
377
+ numFields: lastResponse.metadata.numFields,
378
+ skip: paginationType === "skip" ? 0 : undefined,
379
+ top: paginationType === "skip" ? undefined : undefined,
380
+ ...(paginationType === "token" &&
381
+ continuation?.type === "token" &&
382
+ continuation.key
383
+ ? { [continuation.key]: "" }
384
+ : {}),
385
+ },
386
+ };
387
+
388
+ if (debug) {
389
+ console.log("[paginateRecords] Final response summary:", {
390
+ totalRecords: finalResponse.metadata.totalRecords,
391
+ numRecords: finalResponse.metadata.numRecords,
392
+ numFields: finalResponse.metadata.numFields,
393
+ skip: finalResponse.metadata.skip,
394
+ top: finalResponse.metadata.top,
395
+ ...(paginationType === "token" &&
396
+ continuation?.type === "token" &&
397
+ continuation.key
398
+ ? { [continuation.key]: finalResponse.metadata[continuation.key] }
399
+ : {}),
400
+ });
401
+ console.log("[paginateRecords] Pagination stats:", {
402
+ totalRequests: requestCount,
403
+ });
404
+ }
405
+
406
+ return finalResponse as ReturnType<QuickbaseClient[K]>;
407
+ }
408
+
409
+ /**
410
+ * Checks if a response is paginatable based on its structure.
411
+ * @param response - The response to check.
412
+ * @returns True if the response has a data array and pagination metadata, false otherwise.
413
+ */
414
+ export function isPaginatable(
415
+ response: any
416
+ ): response is PaginatedResponse<any> {
417
+ return (
418
+ response &&
419
+ typeof response === "object" &&
420
+ Object.keys(response).some((key) => Array.isArray(response[key])) &&
421
+ "metadata" in response &&
422
+ typeof response.metadata === "object" &&
423
+ (("totalRecords" in response.metadata &&
424
+ "numRecords" in response.metadata &&
425
+ "skip" in response.metadata) ||
426
+ "nextPageToken" in response.metadata ||
427
+ "nextToken" in response.metadata)
428
+ );
429
+ }
@@ -1,4 +1,3 @@
1
- // src/quickbaseClient.ts
2
1
  import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient";
3
2
  import { Configuration, HTTPHeaders } from "./generated/runtime";
4
3
  import * as apis from "./generated/apis";
@@ -16,7 +15,8 @@ import {
16
15
  AuthorizationStrategy,
17
16
  SsoTokenStrategy,
18
17
  } from "./authorizationStrategy";
19
- import { ThrottleBucket } from "./ThrottleBucket";
18
+ import { FlowThrottleBucket } from "./FlowThrottleBucket";
19
+ import { BurstAwareThrottleBucket } from "./BurstAwareThrottleBucket";
20
20
  import { RateLimiter } from "./rateLimiter";
21
21
 
22
22
  export * from "./generated/models/index";
@@ -34,13 +34,26 @@ export interface QuickbaseConfig {
34
34
  fetchApi?: typeof fetch;
35
35
  convertDates?: boolean;
36
36
  tempTokenLifespan?: number;
37
- throttle?: { rate: number; burst: number };
37
+ throttle?: {
38
+ type?: "flow" | "burst-aware";
39
+ rate?: number;
40
+ burst?: number;
41
+ windowSeconds?: number;
42
+ };
38
43
  maxRetries?: number;
39
44
  retryDelay?: number;
40
45
  tokenCache?: TokenCache;
41
46
  baseUrl?: string;
47
+ autoPaginate?: boolean;
42
48
  }
43
49
 
50
+ export type ThrottleOptions = {
51
+ type?: "flow" | "burst-aware";
52
+ rate?: number;
53
+ burst?: number;
54
+ windowSeconds?: number;
55
+ };
56
+
44
57
  type MethodMap = {
45
58
  [K in keyof QuickbaseClient]: MethodInfo<K>;
46
59
  };
@@ -50,35 +63,45 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
50
63
  realm,
51
64
  userToken,
52
65
  tempToken,
53
- useTempTokens,
54
- useSso,
66
+ useTempTokens = false,
67
+ useSso = false,
55
68
  samlToken,
56
69
  fetchApi,
57
70
  debug,
58
71
  convertDates = true,
59
72
  tempTokenLifespan = 290000,
60
- throttle = { rate: 10, burst: 10 },
73
+ throttle = { type: "flow", rate: 5, burst: 50 }, // Changed rate from 6 to 5
61
74
  maxRetries = 3,
62
75
  retryDelay = 1000,
63
76
  tokenCache: providedTokenCache,
64
77
  baseUrl = "https://api.quickbase.com/v1",
78
+ autoPaginate = true,
65
79
  } = config;
66
80
 
67
81
  const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
68
- const throttleBucket = throttle
69
- ? new ThrottleBucket(throttle.rate, throttle.burst)
70
- : null;
71
- const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
72
82
 
73
- const defaultFetch: typeof fetch | undefined =
74
- typeof globalThis.window !== "undefined"
75
- ? globalThis.window.fetch.bind(globalThis.window)
76
- : undefined;
77
- const effectiveFetch = fetchApi || defaultFetch;
83
+ const throttleOptions = throttle as ThrottleOptions;
84
+ const throttleBucket =
85
+ throttleOptions.type === "burst-aware"
86
+ ? new BurstAwareThrottleBucket({
87
+ maxTokens: throttleOptions.burst || 100,
88
+ windowSeconds: throttleOptions.windowSeconds || 10,
89
+ })
90
+ : new FlowThrottleBucket(
91
+ throttleOptions.rate || 5, // Changed rate from 6 to 5
92
+ throttleOptions.burst || 50
93
+ );
78
94
 
79
- if (!effectiveFetch) {
80
- throw new Error("No fetch implementation available");
95
+ const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
96
+
97
+ const defaultFetch: typeof fetch =
98
+ globalThis.fetch || globalThis.window?.fetch?.bind(globalThis.window);
99
+ if (!defaultFetch && !fetchApi) {
100
+ throw new Error(
101
+ "No fetch implementation available. Please provide fetchApi in a Node.js environment without native fetch."
102
+ );
81
103
  }
104
+ const effectiveFetch = fetchApi || defaultFetch;
82
105
 
83
106
  const authStrategy: AuthorizationStrategy = useSso
84
107
  ? new SsoTokenStrategy(
@@ -160,14 +183,16 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
160
183
 
161
184
  const methodMap = buildMethodMap();
162
185
 
163
- const proxyHandler: ProxyHandler<QuickbaseClient> = {
164
- get: (_, prop: string) => {
165
- console.log(
166
- "[proxy] Accessing:",
167
- prop,
168
- "in methodMap:",
169
- prop in methodMap
170
- );
186
+ const proxyHandler: globalThis.ProxyHandler<QuickbaseClient> = {
187
+ get: (_: QuickbaseClient, prop: string) => {
188
+ if (debug) {
189
+ console.log(
190
+ "[proxy] Accessing:",
191
+ prop,
192
+ "in methodMap:",
193
+ prop in methodMap
194
+ );
195
+ }
171
196
  if (prop in methodMap) {
172
197
  const methodName = prop as keyof QuickbaseClient;
173
198
  return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) =>
@@ -180,10 +205,13 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
180
205
  rateLimiter,
181
206
  transformDates,
182
207
  debug,
183
- convertDates
208
+ convertDates,
209
+ autoPaginate
184
210
  );
185
211
  }
186
- console.log("[proxy] Method not found:", prop);
212
+ if (debug) {
213
+ console.log("[proxy] Method not found:", prop);
214
+ }
187
215
  return undefined;
188
216
  },
189
217
  };
@@ -192,7 +220,7 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
192
220
 
193
221
  if (debug) {
194
222
  console.log("[createClient] Config:", config);
195
- console.log("[createClient] Proxy created:", proxy);
223
+ console.log("[createClient] Returning proxy");
196
224
  }
197
225
 
198
226
  return proxy;
@@ -1,10 +1,10 @@
1
- // src/rateLimiter.ts
2
- import { ThrottleBucket } from "./ThrottleBucket";
1
+ // src/RateLimiter.ts
3
2
  import { ResponseError } from "./generated/runtime";
3
+ import { RateThrottleBucket } from "./RateThrottleBucket";
4
4
 
5
5
  export class RateLimiter {
6
6
  constructor(
7
- private throttleBucket: ThrottleBucket | null,
7
+ private throttleBucket: RateThrottleBucket | null,
8
8
  public maxRetries: number = 3,
9
9
  private retryDelay: number = 1000
10
10
  ) {}
@@ -15,8 +15,13 @@ export class RateLimiter {
15
15
  }
16
16
  }
17
17
 
18
+ release(): void {
19
+ if (this.throttleBucket) {
20
+ this.throttleBucket.release();
21
+ }
22
+ }
23
+
18
24
  async handle429(error: ResponseError, attempt: number): Promise<number> {
19
- // Remove the throw here; let invokeMethod handle exhaustion
20
25
  const retryAfter = error.response.headers.get("Retry-After");
21
26
  return retryAfter
22
27
  ? parseInt(retryAfter, 10) * 1000
@@ -28,7 +28,7 @@ describe("QuickbaseClient Integration - getFieldsUsage", () => {
28
28
  console.log(
29
29
  `Retrieved ${response.length} field usage entries for table ${QB_TABLE_ID_1}`
30
30
  );
31
- }, 15000); // 15-second timeout
31
+ }, 30000); // 15-second timeout
32
32
 
33
33
  it("retrieves field usage with skip and top parameters", async () => {
34
34
  console.log(
@@ -73,5 +73,5 @@ describe("QuickbaseClient Integration - getFieldsUsage", () => {
73
73
  ),
74
74
  });
75
75
  console.log(`Confirmed 401 for invalid table ID ${invalidTableId}`);
76
- }, 15000); // 15-second timeout
76
+ }, 30000); // 15-second timeout
77
77
  });