quickbase-js 0.1.0-beta.7 → 0.1.0-beta.8

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 (60) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/esm/quickbase.d.ts +589 -125
  3. package/dist/esm/quickbase.js +484 -145
  4. package/dist/esm/quickbase.js.map +1 -1
  5. package/dist/esm/quickbase.min.js +1 -1
  6. package/dist/esm/quickbase.min.js.map +1 -1
  7. package/dist/umd/quickbase.umd.js +492 -144
  8. package/dist/umd/quickbase.umd.js.map +1 -1
  9. package/dist/umd/quickbase.umd.min.js +1 -1
  10. package/dist/umd/quickbase.umd.min.js.map +1 -1
  11. package/open-api/fix-spec-main.ts +69 -68
  12. package/open-api/generate-override-report.ts +214 -0
  13. package/open-api/generate-unified-interface.ts +73 -94
  14. package/open-api/output/override-report.html +11 -0
  15. package/open-api/output/override-report.json +14395 -58
  16. package/open-api/output/quickbase-fixed.json +583 -2
  17. package/open-api/schema/tags/fields.ts +331 -16
  18. package/open-api/schema/tags/index.ts +2 -4
  19. package/open-api/types/spec.ts +2 -1
  20. package/package.json +31 -33
  21. package/src/RateLimitError.ts +12 -0
  22. package/src/ThrottleBucket.ts +36 -0
  23. package/src/generated-unified/QuickbaseClient.ts +119 -119
  24. package/src/invokeMethod.ts +357 -0
  25. package/src/quickbaseClient.ts +51 -209
  26. package/tests/setup.ts +2 -0
  27. package/tests/vitest/qb/apps/getApp.test.ts +6 -2
  28. package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
  29. package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
  30. package/tests/vitest/qb/auth/retryUserToken401.test.ts +56 -0
  31. package/tests/vitest/qb/fields/createField.test.ts +1 -2
  32. package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
  33. package/tests/vitest/qb/fields/updateField.test.ts +93 -0
  34. package/tests/vitest/qb/index.ts +1 -0
  35. package/tests/vitest/qb/tables/updateRelationship.test.ts +277 -0
  36. package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
  37. package/tests/vitest/unit/apps/updateApp.test.ts +381 -0
  38. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +6 -26
  39. package/tests/vitest/unit/auth/{retryOn401.test.ts → retryTempToken401.test.ts} +3 -6
  40. package/tests/vitest/unit/auth/{two401s.test.ts → retryTempTokenTwo401.test.ts} +4 -9
  41. package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -0
  42. package/tests/vitest/unit/fields/createField.test.ts +8 -12
  43. package/tests/vitest/unit/fields/getFieldUsage.test.ts +33 -14
  44. package/tests/vitest/unit/fields/updateField.test.ts +280 -0
  45. package/tests/vitest/unit/rateLimit.test.ts +189 -0
  46. package/tests/vitest/unit/tables/updateRelationship.test.ts +305 -0
  47. package/tests/vitest/unit/tokenLifespan.test.ts +138 -0
  48. package/tools/log-gen.ts +8 -4
  49. package/tools/log-gen.yaml +3004 -1848
  50. package/tsconfig.json +5 -2
  51. package/vitest.config.ts +2 -2
  52. package/notes.txt +0 -158
  53. package/tools/generate-types.ts +0 -29
  54. package/tools/openapi-schema-validator.ts +0 -38
  55. package/tools/spec-analyzer.ts +0 -163
  56. package/tools/split-spec.ts +0 -148
  57. package/tools/test-import.ts +0 -3
  58. package/tools/validate-spec.ts +0 -40
  59. /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
  60. /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
@@ -0,0 +1,357 @@
1
+ // src/invokeMethod.ts
2
+ import { QuickbaseClient } from "./quickbaseClient";
3
+ import { ResponseError } from "./generated/runtime";
4
+ import { ThrottleBucket } from "./ThrottleBucket";
5
+ import { RateLimitError } from "./RateLimitError";
6
+ import { GetTempTokenDBID200Response } from "./generated/models";
7
+
8
+ export type ApiMethod<K extends keyof QuickbaseClient> = (
9
+ requestParameters: Parameters<QuickbaseClient[K]>[0],
10
+ initOverrides?: RequestInit
11
+ ) => Promise<ReturnType<QuickbaseClient[K]>>;
12
+
13
+ export interface MethodInfo<K extends keyof QuickbaseClient> {
14
+ api: any;
15
+ method: ApiMethod<K>;
16
+ paramMap: string[];
17
+ httpMethod: string;
18
+ }
19
+
20
+ export interface TempTokenParams {
21
+ appId?: string;
22
+ tableId?: string;
23
+ dbid?: string;
24
+ }
25
+
26
+ export interface TokenCache {
27
+ get: (dbid: string) => string | undefined;
28
+ set: (dbid: string, token: string) => void;
29
+ }
30
+
31
+ export async function invokeMethod<K extends keyof QuickbaseClient>(
32
+ methodName: K,
33
+ params: Parameters<QuickbaseClient[K]>[0] & Partial<TempTokenParams>,
34
+ methodMap: { [P in keyof QuickbaseClient]: MethodInfo<P> },
35
+ baseHeaders: Record<string, string>,
36
+ tokenCache: TokenCache,
37
+ fetchTempToken: (dbid: string) => Promise<string>,
38
+ transformDates: (obj: any, convertStringsToDates: boolean) => any,
39
+ initialTempToken: string | undefined,
40
+ userToken: string | undefined,
41
+ useTempTokens: boolean | undefined,
42
+ debug: boolean | undefined,
43
+ convertDates: boolean,
44
+ retryCount: number = 0,
45
+ throttleBucket: ThrottleBucket | null = null,
46
+ maxRetries: number = 3,
47
+ retryDelay: number = 1000
48
+ ): Promise<ReturnType<QuickbaseClient[K]>> {
49
+ const methodInfo = methodMap[methodName];
50
+ if (!methodInfo) throw new Error(`Method ${methodName} not found`);
51
+
52
+ const hasBody = "body" in params && params.body !== undefined;
53
+ const body = hasBody ? (params as any).body : undefined;
54
+ const restParams: any = hasBody
55
+ ? Object.fromEntries(
56
+ Object.entries(params).filter(([key]) => key !== "body")
57
+ )
58
+ : { ...params };
59
+
60
+ const requestParameters: any = {
61
+ ...restParams,
62
+ ...(hasBody ? { generated: body } : {}),
63
+ };
64
+
65
+ let requestOptions: RequestInit = {
66
+ credentials: "omit",
67
+ method: methodInfo.httpMethod,
68
+ ...(hasBody ? { body } : {}),
69
+ };
70
+
71
+ const selectedToken =
72
+ initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
73
+ let authorizationToken = selectedToken;
74
+
75
+ if (methodName === "getTempTokenDBID" && useTempTokens) {
76
+ const dbid = extractDbid(params, "No dbid provided for getTempTokenDBID");
77
+ const cachedToken = tokenCache.get(dbid);
78
+ if (cachedToken) {
79
+ return Promise.resolve({
80
+ temporaryAuthorization: cachedToken,
81
+ } as GetTempTokenDBID200Response as ReturnType<QuickbaseClient[K]>);
82
+ }
83
+ }
84
+
85
+ if (useTempTokens && !authorizationToken) {
86
+ const dbid = extractDbid(
87
+ params,
88
+ `No dbid found in params for ${methodName} to fetch temp token`
89
+ );
90
+ const cachedToken = tokenCache.get(dbid);
91
+ authorizationToken = cachedToken || (await fetchTempToken(dbid));
92
+ if (methodName === "getTempTokenDBID") {
93
+ return Promise.resolve({
94
+ temporaryAuthorization: authorizationToken,
95
+ } as GetTempTokenDBID200Response as ReturnType<QuickbaseClient[K]>);
96
+ }
97
+ }
98
+ requestOptions.headers = authorizationToken
99
+ ? {
100
+ ...baseHeaders,
101
+ Authorization: useTempTokens
102
+ ? `QB-TEMP-TOKEN ${authorizationToken}`
103
+ : `QB-USER-TOKEN ${authorizationToken}`,
104
+ }
105
+ : baseHeaders;
106
+
107
+ if (debug) {
108
+ console.log(`[${methodName}] requestParameters:`, requestParameters);
109
+ console.log(`[${methodName}] requestOptions:`, requestOptions);
110
+ }
111
+
112
+ async function processResponse(
113
+ rawResponse: any
114
+ ): Promise<ReturnType<QuickbaseClient[K]>> {
115
+ if (debug) console.log(`[${methodName}] rawResponse:`, rawResponse);
116
+
117
+ if (rawResponse instanceof Response) {
118
+ const contentType = rawResponse.headers
119
+ .get("Content-Type")
120
+ ?.toLowerCase();
121
+ if (debug) console.log(`[${methodName}] contentType:`, contentType);
122
+
123
+ if (contentType?.includes("application/octet-stream")) {
124
+ return (await rawResponse.arrayBuffer()) as ReturnType<
125
+ QuickbaseClient[K]
126
+ >;
127
+ }
128
+ if (
129
+ contentType?.includes("application/x-yaml") ||
130
+ contentType?.includes("text/yaml")
131
+ ) {
132
+ return (await rawResponse.text()) as ReturnType<QuickbaseClient[K]>;
133
+ }
134
+ if (contentType?.includes("application/json")) {
135
+ const jsonResponse = await rawResponse.json();
136
+ return transformDates(jsonResponse, convertDates) as ReturnType<
137
+ QuickbaseClient[K]
138
+ >;
139
+ }
140
+ return rawResponse as ReturnType<QuickbaseClient[K]>;
141
+ }
142
+
143
+ if (rawResponse && typeof rawResponse.value === "function") {
144
+ const response = await rawResponse.value();
145
+ if (debug)
146
+ console.log(`[${methodName}] Resolved JSONApiResponse:`, response);
147
+ return transformDates(response, convertDates) as ReturnType<
148
+ QuickbaseClient[K]
149
+ >;
150
+ }
151
+
152
+ const transformed = transformDates(rawResponse, convertDates);
153
+ if (debug)
154
+ console.log(`[${methodName}] Transformed non-Response:`, transformed);
155
+ return transformed as ReturnType<QuickbaseClient[K]>;
156
+ }
157
+
158
+ async function withRetries(
159
+ fn: () => Promise<ReturnType<QuickbaseClient[K]>>,
160
+ options: {
161
+ maxAttempts: number;
162
+ shouldRetry: (error: any) => boolean;
163
+ onRetry?: (error: any, attempt: number) => Promise<void>;
164
+ delay?: (attempt: number, error: any) => number;
165
+ }
166
+ ): Promise<ReturnType<QuickbaseClient[K]>> {
167
+ let attempt = 0;
168
+ while (true) {
169
+ try {
170
+ if (throttleBucket) {
171
+ if (debug) console.log(`[${methodName}] Awaiting throttle bucket`);
172
+ await throttleBucket.acquire();
173
+ if (debug) console.log(`[${methodName}] Throttle bucket acquired`);
174
+ }
175
+ return await fn();
176
+ } catch (error) {
177
+ console.log(`[${methodName}] Caught error:`, error);
178
+ console.log(
179
+ `[${methodName}] instanceof ResponseError:`,
180
+ error instanceof ResponseError
181
+ );
182
+ console.log(
183
+ `[${methodName}] Response status:`,
184
+ (error as any).response?.status
185
+ );
186
+ const effectiveError =
187
+ error instanceof Object &&
188
+ "cause" in error &&
189
+ error.cause instanceof ResponseError
190
+ ? error.cause
191
+ : error instanceof ResponseError
192
+ ? error
193
+ : error;
194
+ console.log(`[${methodName}] Effective error:`, effectiveError);
195
+ console.log(
196
+ `[${methodName}] Effective instanceof ResponseError:`,
197
+ effectiveError instanceof ResponseError
198
+ );
199
+ console.log(
200
+ `[${methodName}] Effective response status:`,
201
+ (effectiveError as any).response?.status
202
+ );
203
+
204
+ attempt++;
205
+ if (
206
+ !options.shouldRetry(effectiveError) ||
207
+ attempt >= options.maxAttempts
208
+ ) {
209
+ let errorMessage =
210
+ effectiveError instanceof ResponseError
211
+ ? effectiveError.message
212
+ : String(error);
213
+ console.log(`[${methodName}] No retry, throwing:`, errorMessage);
214
+ if (
215
+ effectiveError instanceof ResponseError &&
216
+ effectiveError.response
217
+ ) {
218
+ try {
219
+ const errorBody = await effectiveError.response.json();
220
+ if (debug)
221
+ console.log(`[${methodName}] Error response body:`, errorBody);
222
+ errorMessage = errorBody?.message || errorMessage;
223
+ } catch (e) {
224
+ if (debug)
225
+ console.log(`[${methodName}] Failed to parse error body:`, e);
226
+ }
227
+ throw new Error(
228
+ `API Error: ${errorMessage} (Status: ${effectiveError.response.status})`
229
+ );
230
+ }
231
+ throw new Error(`API Error: ${errorMessage} (Status: unknown)`);
232
+ }
233
+ if (options.onRetry) {
234
+ console.log(`[${methodName}] Retrying, attempt:`, attempt);
235
+ await options.onRetry(effectiveError, attempt);
236
+ }
237
+ const delayMs = options.delay
238
+ ? options.delay(attempt, effectiveError)
239
+ : retryDelay * Math.pow(2, attempt - 1);
240
+ if (debug)
241
+ console.log(
242
+ `[${methodName}] Retrying after ${delayMs}ms (attempt ${attempt}/${
243
+ options.maxAttempts - 1
244
+ })`
245
+ );
246
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
247
+ }
248
+ }
249
+ }
250
+
251
+ const mainPromise = withRetries(
252
+ () =>
253
+ methodInfo
254
+ .method(requestParameters, requestOptions)
255
+ .then(processResponse),
256
+ {
257
+ maxAttempts: (useTempTokens ?? false) || userToken ? 2 : 1,
258
+ shouldRetry: (error) =>
259
+ error instanceof ResponseError &&
260
+ error.response?.status === 401 &&
261
+ ((useTempTokens ?? false) ||
262
+ (!!userToken && !(useTempTokens ?? false))),
263
+ onRetry: async (error) => {
264
+ if (error instanceof ResponseError && error.response?.status === 401) {
265
+ if (useTempTokens ?? false) {
266
+ if (debug)
267
+ console.log(
268
+ `Authorization error for ${methodName} (temp token), refreshing token:`,
269
+ error.message
270
+ );
271
+ const dbid = extractDbid(
272
+ params,
273
+ `No dbid to refresh token after authorization error`
274
+ );
275
+ try {
276
+ authorizationToken = await fetchTempToken(dbid);
277
+ } catch (fetchError) {
278
+ const fetchErrorMessage =
279
+ fetchError instanceof ResponseError && fetchError.response
280
+ ? (await fetchError.response.json())?.message ||
281
+ "Unauthorized"
282
+ : String(fetchError);
283
+ throw new Error(
284
+ `API Error: ${fetchErrorMessage} (Status: ${
285
+ fetchError instanceof ResponseError &&
286
+ fetchError.response?.status
287
+ ? fetchError.response.status
288
+ : "unknown"
289
+ })`
290
+ );
291
+ }
292
+ requestOptions.headers = {
293
+ ...baseHeaders,
294
+ Authorization: `QB-TEMP-TOKEN ${authorizationToken}`,
295
+ };
296
+ } else if (userToken) {
297
+ if (debug)
298
+ console.log(
299
+ `Authorization error for ${methodName} (user token), retrying with same token:`,
300
+ error.message
301
+ );
302
+ }
303
+ if (debug)
304
+ console.log(
305
+ `Retrying ${methodName} with ${
306
+ useTempTokens ? "temp" : "user"
307
+ } token`
308
+ );
309
+ }
310
+ },
311
+ }
312
+ );
313
+
314
+ return mainPromise.catch((error: unknown) => {
315
+ let effectiveError: unknown = error;
316
+ if (
317
+ error instanceof Object &&
318
+ "cause" in error &&
319
+ error.cause instanceof ResponseError
320
+ ) {
321
+ effectiveError = error.cause;
322
+ } else if (error instanceof ResponseError) {
323
+ effectiveError = error;
324
+ }
325
+
326
+ if (
327
+ effectiveError instanceof ResponseError &&
328
+ effectiveError.response?.status === 429
329
+ ) {
330
+ return withRetries(() => Promise.reject(error), {
331
+ maxAttempts: maxRetries + 1,
332
+ shouldRetry: (err) =>
333
+ err instanceof ResponseError && err.response?.status === 429,
334
+ delay: (attempt, err) => {
335
+ const headers =
336
+ err instanceof ResponseError && err.response
337
+ ? err.response.headers
338
+ : new Headers();
339
+ const retryAfter = headers.get("Retry-After");
340
+ return retryAfter
341
+ ? parseInt(retryAfter, 10) * 1000
342
+ : retryDelay * Math.pow(2, attempt - 1);
343
+ },
344
+ });
345
+ }
346
+ throw effectiveError;
347
+ });
348
+ }
349
+
350
+ function extractDbid(
351
+ params: Partial<TempTokenParams>,
352
+ errorMessage: string
353
+ ): string {
354
+ const dbid = params.dbid || params.tableId || params.appId;
355
+ if (!dbid) throw new Error(errorMessage);
356
+ return dbid;
357
+ }
@@ -1,9 +1,11 @@
1
1
  // src/quickbaseClient.ts
2
2
  import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient";
3
- import { Configuration, HTTPHeaders, ResponseError } from "./generated/runtime";
3
+ import { Configuration, HTTPHeaders } from "./generated/runtime";
4
4
  import * as apis from "./generated/apis";
5
5
  import { TokenCache } from "./tokenCache";
6
6
  import { simplifyName } from "./utils";
7
+ import { invokeMethod, MethodInfo } from "./invokeMethod"; // Added import
8
+ import { ThrottleBucket } from "./ThrottleBucket";
7
9
 
8
10
  export * from "./generated/models/index";
9
11
 
@@ -16,24 +18,11 @@ export interface QuickbaseConfig {
16
18
  useTempTokens?: boolean;
17
19
  debug?: boolean;
18
20
  fetchApi?: typeof fetch;
19
- convertDates?: boolean; // New flag to control date string conversion, defaults to true
20
- }
21
-
22
- export interface TempTokenParams {
23
- appId?: string;
24
- tableId?: string;
25
- dbid?: string;
26
- }
27
-
28
- type ApiMethod<K extends keyof QuickbaseClient> = (
29
- requestParameters: Parameters<QuickbaseClient[K]>[0],
30
- initOverrides?: RequestInit
31
- ) => Promise<ReturnType<QuickbaseClient[K]>>;
32
-
33
- interface MethodInfo<K extends keyof QuickbaseClient> {
34
- api: any;
35
- method: ApiMethod<K>;
36
- paramMap: string[];
21
+ convertDates?: boolean;
22
+ tokenLifespan?: number;
23
+ throttle?: { rate: number; burst: number };
24
+ maxRetries?: number;
25
+ retryDelay?: number;
37
26
  }
38
27
 
39
28
  type MethodMap = {
@@ -48,18 +37,6 @@ const getParamNames = (fn: (...args: any[]) => any): string[] =>
48
37
  .map((p) => p.trim().split("=")[0]?.trim())
49
38
  .filter((p) => p && !p.match(/^\{/) && p !== "options");
50
39
 
51
- const extractDbid = (
52
- params: Partial<TempTokenParams>,
53
- errorMessage: string
54
- ): string => {
55
- const dbid = params.dbid || params.tableId || params.appId;
56
- if (!dbid) {
57
- throw new Error(errorMessage);
58
- }
59
- return dbid;
60
- };
61
-
62
- // Utility to convert ISO date strings to Date objects recursively, with optional conversion
63
40
  function transformDates(obj: any, convertStringsToDates: boolean = true): any {
64
41
  if (obj === null || obj === undefined) return obj;
65
42
  if (obj instanceof Date) return obj;
@@ -86,6 +63,16 @@ function transformDates(obj: any, convertStringsToDates: boolean = true): any {
86
63
  return obj;
87
64
  }
88
65
 
66
+ function inferHttpMethod(methodSource: string, debug?: boolean): string {
67
+ const methodMatch = methodSource.match(/method:\s*['"]?(\w+)['"]?/i);
68
+ const method = methodMatch ? methodMatch[1].toUpperCase() : "GET";
69
+ if (debug) {
70
+ // console.log(`[inferHttpMethod] Source:`, methodSource);
71
+ // console.log(`[inferHttpMethod] Extracted method:`, method);
72
+ }
73
+ return method;
74
+ }
75
+
89
76
  export function quickbase(config: QuickbaseConfig): QuickbaseClient {
90
77
  const {
91
78
  realm,
@@ -94,11 +81,18 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
94
81
  useTempTokens,
95
82
  fetchApi,
96
83
  debug,
97
- convertDates = true, // Default to true for backward compatibility
84
+ convertDates = true,
85
+ tokenLifespan,
86
+ throttle = { rate: 10, burst: 10 },
87
+ maxRetries = 3,
88
+ retryDelay = 1000,
98
89
  } = config;
99
90
  const baseUrl = `https://api.quickbase.com/v1`;
100
91
 
101
- const tokenCache = new TokenCache();
92
+ const tokenCache = new TokenCache(tokenLifespan);
93
+ const throttleBucket = throttle
94
+ ? new ThrottleBucket(throttle.rate, throttle.burst)
95
+ : null;
102
96
 
103
97
  const baseHeaders: HTTPHeaders = {
104
98
  "QB-Realm-Hostname": `${realm}.quickbase.com`,
@@ -157,13 +151,17 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
157
151
  const simplifiedName = simplifyName(
158
152
  rawMethodName
159
153
  ) as keyof QuickbaseClient;
160
- const method = api[rawMethodName as keyof typeof api];
154
+ const rawMethodKey = `${rawMethodName}Raw` as keyof typeof api;
155
+ const method =
156
+ api[rawMethodKey] || api[rawMethodName as keyof typeof api];
161
157
  const boundMethod = method.bind(api as any) as unknown;
162
158
  if (typeof boundMethod === "function" && boundMethod.length <= 2) {
159
+ const methodSource = method.toString();
163
160
  methodMap[simplifiedName] = {
164
161
  api,
165
- method: boundMethod as ApiMethod<typeof simplifiedName>,
162
+ method: boundMethod as any, // Adjust typing as needed
166
163
  paramMap: getParamNames(method),
164
+ httpMethod: inferHttpMethod(methodSource, debug),
167
165
  };
168
166
  }
169
167
  });
@@ -209,185 +207,29 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
209
207
  return token;
210
208
  };
211
209
 
212
- async function invokeMethod<K extends keyof QuickbaseClient>(
213
- methodName: K,
214
- params: Parameters<QuickbaseClient[K]>[0] & Partial<TempTokenParams>,
215
- retryCount: number = 0
216
- ): Promise<ReturnType<QuickbaseClient[K]>> {
217
- const methodInfo = methodMap[methodName];
218
- if (!methodInfo) {
219
- throw new Error(`Method ${methodName} not found`);
220
- }
221
-
222
- // Safely handle body extraction
223
- const hasBody = "body" in params && params.body !== undefined;
224
- const body = hasBody ? (params as any).body : undefined;
225
- const restParams: any = hasBody
226
- ? Object.fromEntries(
227
- Object.entries(params).filter(([key]) => key !== "body")
228
- )
229
- : { ...params };
230
-
231
- // Construct requestParameters with 'generated' for body
232
- const requestParameters: any = {
233
- ...restParams,
234
- ...(body ? { generated: { ...body } } : {}),
235
- };
236
-
237
- let requestOptions: RequestInit = {
238
- credentials: "omit",
239
- };
240
-
241
- const selectedToken =
242
- initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
243
-
244
- if (methodName === "getTempTokenDBID" && useTempTokens) {
245
- const dbid = extractDbid(params, "No dbid provided for getTempTokenDBID");
246
- const cachedToken = tokenCache.get(dbid);
247
- if (cachedToken) {
248
- return { temporaryAuthorization: cachedToken } as ReturnType<
249
- QuickbaseClient[K]
250
- >;
251
- }
252
- }
253
-
254
- let authorizationToken = selectedToken;
255
- if (useTempTokens && !authorizationToken) {
256
- const dbid = extractDbid(
257
- params,
258
- `No dbid found in params for ${methodName} to fetch temp token`
259
- );
260
- const cachedToken = tokenCache.get(dbid);
261
- authorizationToken = cachedToken || (await fetchTempToken(dbid));
262
- if (methodName === "getTempTokenDBID") {
263
- return { temporaryAuthorization: authorizationToken } as ReturnType<
264
- QuickbaseClient[K]
265
- >;
266
- }
267
- requestOptions.headers = {
268
- ...baseHeaders,
269
- Authorization: `QB-TEMP-TOKEN ${authorizationToken}`,
270
- };
271
- } else if (authorizationToken) {
272
- requestOptions.headers = {
273
- ...baseHeaders,
274
- Authorization: `QB-USER-TOKEN ${authorizationToken}`,
275
- };
276
- }
277
-
278
- if (debug) {
279
- console.log(`[${methodName}] requestParameters:`, requestParameters);
280
- console.log(`[${methodName}] requestOptions:`, requestOptions);
281
- }
282
-
283
- try {
284
- const response = await methodInfo.method(
285
- requestParameters,
286
- requestOptions
287
- );
288
- if (debug) {
289
- console.log(`[${methodName}] rawResponse:`, response);
290
- }
291
- if (response instanceof Response) {
292
- const contentType = response.headers.get("Content-Type")?.toLowerCase();
293
- if (debug) {
294
- console.log(`[${methodName}] contentType:`, contentType);
295
- }
296
- if (contentType?.includes("application/octet-stream")) {
297
- return (await response.arrayBuffer()) as ReturnType<
298
- QuickbaseClient[K]
299
- >;
300
- } else if (
301
- contentType?.includes("application/x-yaml") ||
302
- contentType?.includes("text/yaml")
303
- ) {
304
- return (await response.text()) as ReturnType<QuickbaseClient[K]>;
305
- } else if (contentType?.includes("application/json")) {
306
- const jsonResponse = await response.json();
307
- if (debug) {
308
- console.log(`[${methodName}] jsonResponse:`, jsonResponse);
309
- }
310
- const transformedResponse = transformDates(
311
- jsonResponse,
312
- convertDates
313
- );
314
- if (debug) {
315
- console.log(
316
- `[${methodName}] transformedResponse:`,
317
- transformedResponse
318
- );
319
- }
320
- return transformedResponse as ReturnType<QuickbaseClient[K]>;
321
- }
322
- return response as ReturnType<QuickbaseClient[K]>;
323
- } else {
324
- if (debug) {
325
- console.log(
326
- `[${methodName}] non-Response return, applying transform:`,
327
- response
328
- );
329
- }
330
- const transformedResponse = transformDates(response, convertDates);
331
- if (debug) {
332
- console.log(
333
- `[${methodName}] transformedNonResponse:`,
334
- transformedResponse
335
- );
336
- }
337
- return transformedResponse as ReturnType<QuickbaseClient[K]>;
338
- }
339
- } catch (error) {
340
- if (
341
- error instanceof ResponseError &&
342
- error.response.status === 401 &&
343
- retryCount < 1 &&
344
- useTempTokens
345
- ) {
346
- if (debug) {
347
- console.log(
348
- `Authorization error for ${methodName}, refreshing token:`,
349
- error.message
350
- );
351
- }
352
- const dbid = extractDbid(
353
- params,
354
- `No dbid to refresh token after authorization error`
355
- );
356
- authorizationToken = await fetchTempToken(dbid);
357
- requestOptions.headers = {
358
- ...baseHeaders,
359
- Authorization: `QB-TEMP-TOKEN ${authorizationToken}`,
360
- };
361
- if (debug) {
362
- console.log(`Retrying ${methodName} with new token`);
363
- }
364
- return invokeMethod(methodName, params, retryCount + 1);
365
- }
366
- if (error instanceof ResponseError) {
367
- let errorMessage = error.message;
368
- try {
369
- const errorBody: { message?: string } = await error.response.json();
370
- if (debug) {
371
- console.log(`Error response body for ${methodName}:`, errorBody);
372
- }
373
- errorMessage = errorBody.message || errorMessage;
374
- } catch (e) {
375
- // Silent fail on parse error
376
- }
377
- throw new Error(
378
- `API Error: ${errorMessage} (Status: ${error.response.status})`
379
- );
380
- }
381
- throw error;
382
- }
383
- }
384
-
385
210
  const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, {
386
211
  get: (_, prop: string): ((params: any) => Promise<any>) | undefined => {
387
212
  if (prop in methodMap) {
388
213
  const methodName = prop as keyof QuickbaseClient;
389
214
  return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) =>
390
- invokeMethod(methodName, params);
215
+ invokeMethod(
216
+ methodName,
217
+ params,
218
+ methodMap,
219
+ baseHeaders,
220
+ tokenCache,
221
+ fetchTempToken,
222
+ transformDates,
223
+ initialTempToken,
224
+ userToken,
225
+ useTempTokens,
226
+ debug,
227
+ convertDates,
228
+ 0, // retryCount
229
+ throttleBucket,
230
+ maxRetries,
231
+ retryDelay
232
+ );
391
233
  }
392
234
  return undefined;
393
235
  },
package/tests/setup.ts CHANGED
@@ -38,12 +38,14 @@ export const createClient = (
38
38
  userToken,
39
39
  debug: true,
40
40
  fetchApi,
41
+ throttle: { rate: 10, burst: 10 }, // Throttle at 10 req/s with 10 burst capacity
41
42
  ...config, // Allow overrides if provided
42
43
  });
43
44
  console.log("[createClient] Config:", {
44
45
  realm,
45
46
  userToken,
46
47
  debug: true,
48
+ throttle: { rate: 10, burst: 10 },
47
49
  ...config,
48
50
  });
49
51
  console.log("[createClient] Returning:", client);