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

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 (82) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +1292 -125
  4. package/dist/esm/quickbase.js +666 -190
  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 +674 -189
  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/open-api/fix-spec-main.ts +69 -68
  13. package/open-api/generate-override-report.ts +214 -0
  14. package/open-api/generate-unified-interface.ts +162 -103
  15. package/open-api/output/override-report.html +11 -0
  16. package/open-api/output/override-report.json +14395 -58
  17. package/open-api/output/quickbase-fixed.json +583 -2
  18. package/open-api/schema/tags/fields.ts +331 -16
  19. package/open-api/schema/tags/index.ts +2 -4
  20. package/open-api/types/spec.ts +2 -1
  21. package/open-api/utils/generateJsDoc.ts +96 -0
  22. package/package.json +33 -33
  23. package/src/RateLimitError.ts +12 -0
  24. package/src/ThrottleBucket.ts +41 -0
  25. package/src/authorizationStrategy.ts +246 -0
  26. package/src/generated-unified/QuickbaseClient.ts +809 -119
  27. package/src/invokeMethod.ts +242 -0
  28. package/src/quickbaseClient.ts +117 -248
  29. package/src/rateLimiter.ts +25 -0
  30. package/src/tokenCache.ts +10 -26
  31. package/tests/setup.ts +2 -0
  32. package/tests/vitest/qb/apps/getApp.test.ts +6 -2
  33. package/tests/vitest/qb/apps/getAppEvents.test.ts +67 -0
  34. package/tests/vitest/qb/apps/updateApp.test.ts +58 -0
  35. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  36. package/tests/vitest/qb/auth/retryUserToken401.test.ts +73 -0
  37. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  38. package/tests/vitest/qb/fields/createField.test.ts +1 -2
  39. package/tests/vitest/qb/fields/deleteFields.test.ts +10 -16
  40. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  41. package/tests/vitest/qb/fields/updateField.test.ts +93 -0
  42. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  43. package/tests/vitest/qb/index.ts +1 -0
  44. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  45. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  46. package/tests/vitest/qb/tables/updateRelationship.test.ts +250 -0
  47. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  48. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  49. package/tests/vitest/unit/apps/getAppEvents.test.ts +193 -0
  50. package/tests/vitest/unit/apps/updateApp.test.ts +412 -0
  51. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +7 -26
  52. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  53. package/tests/vitest/unit/auth/retryTempToken401.test.ts +95 -0
  54. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  55. package/tests/vitest/unit/auth/retryUserToken401.test.ts +69 -0
  56. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  57. package/tests/vitest/unit/fields/createField.test.ts +180 -120
  58. package/tests/vitest/unit/fields/getFieldUsage.test.ts +47 -14
  59. package/tests/vitest/unit/fields/updateField.test.ts +285 -0
  60. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  61. package/tests/vitest/unit/index.ts +2 -1
  62. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  63. package/tests/vitest/unit/rateLimit.test.ts +191 -0
  64. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  65. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  66. package/tests/vitest/unit/tables/updateRelationship.test.ts +307 -0
  67. package/tests/vitest/unit/tokenLifespan.test.ts +204 -0
  68. package/tools/log-gen.ts +20 -7
  69. package/tools/log-gen.yaml +3446 -1848
  70. package/tsconfig.json +5 -2
  71. package/vitest.config.ts +2 -2
  72. package/notes.txt +0 -158
  73. package/tests/vitest/unit/auth/retryOn401.test.ts +0 -116
  74. package/tests/vitest/unit/auth/two401s.test.ts +0 -86
  75. package/tools/generate-types.ts +0 -29
  76. package/tools/openapi-schema-validator.ts +0 -38
  77. package/tools/spec-analyzer.ts +0 -163
  78. package/tools/split-spec.ts +0 -148
  79. package/tools/test-import.ts +0 -3
  80. package/tools/validate-spec.ts +0 -40
  81. /package/tests/playwright/qb/auth/{tokenPrefetch.test.ts → tempTokenPrefetch.test.ts} +0 -0
  82. /package/tests/playwright/qb/auth/{tokenRenewal.test.ts → tempTokenRenewal.test.ts} +0 -0
@@ -1,9 +1,19 @@
1
1
  // src/quickbaseClient.ts
2
+
2
3
  import { QuickbaseClient as IQuickbaseClient } from "./generated-unified/QuickbaseClient";
3
- import { Configuration, HTTPHeaders, ResponseError } from "./generated/runtime";
4
+ import { Configuration, HTTPHeaders } from "./generated/runtime";
4
5
  import * as apis from "./generated/apis";
5
6
  import { TokenCache } from "./tokenCache";
6
7
  import { simplifyName } from "./utils";
8
+ import { invokeMethod, MethodInfo } from "./invokeMethod";
9
+ import {
10
+ TempTokenStrategy,
11
+ UserTokenStrategy,
12
+ AuthorizationStrategy,
13
+ SsoTokenStrategy,
14
+ } from "./authorizationStrategy";
15
+ import { ThrottleBucket } from "./ThrottleBucket";
16
+ import { RateLimiter } from "./rateLimiter";
7
17
 
8
18
  export * from "./generated/models/index";
9
19
 
@@ -14,26 +24,17 @@ export interface QuickbaseConfig {
14
24
  userToken?: string;
15
25
  tempToken?: string;
16
26
  useTempTokens?: boolean;
27
+ useSso?: boolean;
28
+ samlToken?: string;
17
29
  debug?: boolean;
18
30
  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[];
31
+ convertDates?: boolean;
32
+ tempTokenLifespan?: number;
33
+ throttle?: { rate: number; burst: number };
34
+ maxRetries?: number;
35
+ retryDelay?: number;
36
+ tokenCache?: TokenCache;
37
+ baseUrl?: string;
37
38
  }
38
39
 
39
40
  type MethodMap = {
@@ -48,18 +49,6 @@ const getParamNames = (fn: (...args: any[]) => any): string[] =>
48
49
  .map((p) => p.trim().split("=")[0]?.trim())
49
50
  .filter((p) => p && !p.match(/^\{/) && p !== "options");
50
51
 
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
52
  function transformDates(obj: any, convertStringsToDates: boolean = true): any {
64
53
  if (obj === null || obj === undefined) return obj;
65
54
  if (obj instanceof Date) return obj;
@@ -86,47 +75,82 @@ function transformDates(obj: any, convertStringsToDates: boolean = true): any {
86
75
  return obj;
87
76
  }
88
77
 
78
+ function inferHttpMethod(methodSource: string, debug?: boolean): string {
79
+ const methodMatches = [
80
+ ...methodSource.matchAll(/method:\s*['"]?(\w+)['"]?/gi),
81
+ ];
82
+ const method =
83
+ methodMatches.length > 0
84
+ ? methodMatches[methodMatches.length - 1][1].toUpperCase()
85
+ : "GET";
86
+ if (debug) {
87
+ console.log(`[inferHttpMethod] Source:`, methodSource);
88
+ console.log(
89
+ `[inferHttpMethod] All matches:`,
90
+ methodMatches.map((m) => m[0])
91
+ );
92
+ console.log(`[inferHttpMethod] Extracted method:`, method);
93
+ }
94
+ return method;
95
+ }
96
+
89
97
  export function quickbase(config: QuickbaseConfig): QuickbaseClient {
90
98
  const {
91
99
  realm,
92
100
  userToken,
93
- tempToken: initialTempToken,
101
+ tempToken,
94
102
  useTempTokens,
103
+ useSso,
104
+ samlToken,
95
105
  fetchApi,
96
106
  debug,
97
- convertDates = true, // Default to true for backward compatibility
107
+ convertDates = true,
108
+ tempTokenLifespan = 290000,
109
+ throttle = { rate: 10, burst: 10 },
110
+ maxRetries = 3,
111
+ retryDelay = 1000,
112
+ tokenCache: providedTokenCache,
113
+ baseUrl = "https://api.quickbase.com/v1",
98
114
  } = config;
99
- const baseUrl = `https://api.quickbase.com/v1`;
100
115
 
101
- const tokenCache = new TokenCache();
116
+ const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
117
+ const throttleBucket = throttle
118
+ ? new ThrottleBucket(throttle.rate, throttle.burst)
119
+ : null;
120
+ const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
121
+
122
+ const defaultFetch: typeof fetch | undefined =
123
+ typeof globalThis.window !== "undefined"
124
+ ? globalThis.window.fetch.bind(globalThis.window)
125
+ : undefined;
126
+ const effectiveFetch = fetchApi || defaultFetch;
127
+
128
+ if (!effectiveFetch) {
129
+ throw new Error("No fetch implementation available");
130
+ }
131
+
132
+ const authStrategy: AuthorizationStrategy = useSso
133
+ ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
134
+ : useTempTokens
135
+ ? new TempTokenStrategy(tokenCache, tempToken)
136
+ : new UserTokenStrategy(userToken || "");
102
137
 
103
138
  const baseHeaders: HTTPHeaders = {
104
139
  "QB-Realm-Hostname": `${realm}.quickbase.com`,
105
140
  "Content-Type": "application/json",
106
141
  };
107
142
 
108
- if (initialTempToken) {
109
- baseHeaders["Authorization"] = `QB-TEMP-TOKEN ${initialTempToken}`;
110
- } else if (userToken && !useTempTokens) {
111
- baseHeaders["Authorization"] = `QB-USER-TOKEN ${userToken}`;
112
- }
113
-
114
- const defaultFetch: typeof fetch | undefined =
115
- typeof globalThis.window !== "undefined"
116
- ? globalThis.window.fetch.bind(globalThis.window)
117
- : undefined;
118
143
  const configuration = new Configuration({
119
144
  basePath: baseUrl,
120
145
  headers: { ...baseHeaders },
121
- fetchApi: fetchApi || defaultFetch,
146
+ fetchApi: effectiveFetch,
122
147
  credentials: "omit",
123
148
  });
124
149
 
125
- if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
126
- throw new Error(
127
- "fetchApi must be provided in non-browser environments (e.g., Node.js)"
128
- );
129
- }
150
+ // Removed redundant check:
151
+ // if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
152
+ // throw new Error("fetchApi must be provided in non-browser environments (e.g., Node.js)");
153
+ // }
130
154
 
131
155
  const apiInstances = Object.fromEntries(
132
156
  Object.entries(apis)
@@ -137,8 +161,6 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
137
161
  ])
138
162
  );
139
163
 
140
- const methodMap = buildMethodMap();
141
-
142
164
  function buildMethodMap(): MethodMap {
143
165
  const methodMap: Partial<MethodMap> = {};
144
166
  const isValidMethod = (name: string) =>
@@ -157,36 +179,35 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
157
179
  const simplifiedName = simplifyName(
158
180
  rawMethodName
159
181
  ) as keyof QuickbaseClient;
160
- const method = api[rawMethodName as keyof typeof api];
182
+ const rawMethodKey = `${rawMethodName}Raw` as keyof typeof api;
183
+ const method =
184
+ api[rawMethodKey] || api[rawMethodName as keyof typeof api];
161
185
  const boundMethod = method.bind(api as any) as unknown;
162
186
  if (typeof boundMethod === "function" && boundMethod.length <= 2) {
187
+ const methodSource = method.toString();
163
188
  methodMap[simplifiedName] = {
164
189
  api,
165
- method: boundMethod as ApiMethod<typeof simplifiedName>,
190
+ method: boundMethod as any,
166
191
  paramMap: getParamNames(method),
192
+ httpMethod: inferHttpMethod(methodSource, debug),
167
193
  };
168
194
  }
169
195
  });
170
196
  }
197
+ if (debug) {
198
+ console.log("[buildMethodMap] Methods:", Object.keys(methodMap));
199
+ }
171
200
  return methodMap as MethodMap;
172
201
  }
173
202
 
174
- const fetchTempToken = async (dbid: string): Promise<string> => {
175
- const effectiveFetch = fetchApi || defaultFetch;
176
- if (!effectiveFetch) {
177
- throw new Error(
178
- "No fetch implementation available for fetching temp token"
179
- );
180
- }
203
+ const methodMap = buildMethodMap();
181
204
 
182
- const response = await effectiveFetch(
183
- `https://api.quickbase.com/v1/auth/temporary/${dbid}`,
184
- {
185
- method: "GET",
186
- headers: { ...baseHeaders },
187
- credentials: "include",
188
- }
189
- );
205
+ const fetchTempToken = async (dbid: string): Promise<string> => {
206
+ const response = await effectiveFetch(`${baseUrl}/auth/temporary/${dbid}`, {
207
+ method: "GET",
208
+ headers: { ...baseHeaders },
209
+ credentials: "include",
210
+ });
190
211
 
191
212
  if (!response.ok) {
192
213
  const errorBody: { message?: string } = await response.json();
@@ -198,204 +219,52 @@ export function quickbase(config: QuickbaseConfig): QuickbaseClient {
198
219
  }
199
220
 
200
221
  const tokenResult = await response.json();
222
+ console.log(`[fetchTempToken] Response:`, tokenResult);
201
223
  const token = tokenResult.temporaryAuthorization;
202
224
  if (!token) {
203
225
  throw new Error("No temporary token returned from API");
204
226
  }
205
- tokenCache.set(dbid, token);
227
+ tokenCache.set(dbid, token, tempTokenLifespan);
206
228
  if (debug) {
207
229
  console.log(`Fetched and cached new token for dbid: ${dbid}`, token);
208
230
  }
209
231
  return token;
210
232
  };
211
233
 
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
234
+ const proxyHandler: ProxyHandler<QuickbaseClient> = {
235
+ get: (_, prop: string) => {
236
+ console.log(
237
+ "[proxy] Accessing:",
238
+ prop,
239
+ "in methodMap:",
240
+ prop in methodMap
287
241
  );
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
- const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, {
386
- get: (_, prop: string): ((params: any) => Promise<any>) | undefined => {
387
242
  if (prop in methodMap) {
388
243
  const methodName = prop as keyof QuickbaseClient;
389
244
  return (params: Parameters<QuickbaseClient[typeof methodName]>[0]) =>
390
- invokeMethod(methodName, params);
245
+ invokeMethod(
246
+ methodName,
247
+ params,
248
+ methodMap,
249
+ baseHeaders,
250
+ authStrategy,
251
+ rateLimiter,
252
+ fetchTempToken,
253
+ transformDates,
254
+ debug,
255
+ convertDates
256
+ );
391
257
  }
258
+ console.log("[proxy] Method not found:", prop);
392
259
  return undefined;
393
260
  },
394
- });
261
+ };
262
+
263
+ const proxy = new Proxy<QuickbaseClient>({} as QuickbaseClient, proxyHandler);
395
264
 
396
265
  if (debug) {
397
266
  console.log("[createClient] Config:", config);
398
- console.log("[createClient] Returning:", proxy);
267
+ console.log("[createClient] Proxy created:", proxy);
399
268
  }
400
269
 
401
270
  return proxy;
@@ -0,0 +1,25 @@
1
+ // src/rateLimiter.ts
2
+ import { ThrottleBucket } from "./ThrottleBucket";
3
+ import { ResponseError } from "./generated/runtime";
4
+
5
+ export class RateLimiter {
6
+ constructor(
7
+ private throttleBucket: ThrottleBucket | null,
8
+ public maxRetries: number = 3,
9
+ private retryDelay: number = 1000
10
+ ) {}
11
+
12
+ async throttle(): Promise<void> {
13
+ if (this.throttleBucket) {
14
+ await this.throttleBucket.acquire();
15
+ }
16
+ }
17
+
18
+ async handle429(error: ResponseError, attempt: number): Promise<number> {
19
+ // Remove the throw here; let invokeMethod handle exhaustion
20
+ const retryAfter = error.response.headers.get("Retry-After");
21
+ return retryAfter
22
+ ? parseInt(retryAfter, 10) * 1000
23
+ : this.retryDelay * Math.pow(2, attempt - 1);
24
+ }
25
+ }
package/src/tokenCache.ts CHANGED
@@ -1,50 +1,34 @@
1
+ // src/tokenCache.ts
1
2
  interface CachedToken {
2
3
  token: string;
3
- expiresAt: number; // Timestamp in milliseconds
4
+ expiresAt: number;
4
5
  }
5
6
 
6
7
  export class TokenCache {
7
8
  private cache: Map<string, CachedToken>;
8
- private readonly lifespan: number; // Token lifespan in milliseconds
9
+ private readonly tempTokenLifespan: number;
9
10
 
10
- constructor(lifespan: number = 4 * 60 * 1000 + 50 * 1000) {
11
- // Default 4:50
11
+ constructor(tempTokenLifespan: number = 4 * 60 * 1000 + 50 * 1000) {
12
12
  this.cache = new Map<string, CachedToken>();
13
- this.lifespan = lifespan;
13
+ this.tempTokenLifespan = tempTokenLifespan;
14
14
  }
15
15
 
16
16
  get(dbid: string): string | undefined {
17
17
  const entry = this.cache.get(dbid);
18
18
  const now = Date.now();
19
- if (entry && entry.expiresAt > now) {
20
- return entry.token;
21
- }
22
- return undefined; // Expired or not found
19
+ if (entry && entry.expiresAt > now) return entry.token;
20
+ if (entry) this.cache.delete(dbid); // Clean up expired temp tokens
21
+ return undefined;
23
22
  }
24
23
 
25
- // New method to get full entry
26
- getEntry(dbid: string): CachedToken | undefined {
27
- const entry = this.cache.get(dbid);
28
- const now = Date.now();
29
- if (entry && entry.expiresAt > now) {
30
- return entry;
31
- }
32
- return undefined; // Expired or not found
33
- }
34
-
35
- set(dbid: string, token: string): void {
24
+ set(dbid: string, token: string, lifespan?: number): void {
36
25
  const now = Date.now();
37
26
  this.cache.set(dbid, {
38
27
  token,
39
- expiresAt: now + this.lifespan,
28
+ expiresAt: now + (lifespan || this.tempTokenLifespan),
40
29
  });
41
30
  }
42
31
 
43
- dump(): [string, CachedToken][] {
44
- return Array.from(this.cache.entries());
45
- }
46
-
47
- // New method to clear the cache
48
32
  clear(): void {
49
33
  this.cache.clear();
50
34
  }
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);
@@ -1,3 +1,4 @@
1
+ // tests/vitest/qb/apps/getApp.test.ts
1
2
  import { test, expect } from "vitest";
2
3
  import { quickbase } from "../../../../src/quickbaseClient";
3
4
 
@@ -20,8 +21,8 @@ test(
20
21
  expect(result).toEqual({
21
22
  id: getAppId,
22
23
  name: "qb-copy",
23
- created: expect.any(Date), // Matches Date object from client
24
- updated: expect.any(Date), // Matches Date object from client
24
+ created: expect.any(Date),
25
+ updated: expect.any(Date),
25
26
  description: "",
26
27
  timeZone: "(UTC-08:00) Pacific Time (US & Canada)",
27
28
  dateFormat: "MM-DD-YYYY",
@@ -35,6 +36,9 @@ test(
35
36
  mustBeRealmApproved: false,
36
37
  useIPFilter: false,
37
38
  },
39
+ ancestorId: undefined,
40
+ dataClassification: undefined,
41
+ variables: [{ name: "TestVar", value: "TestValue" }],
38
42
  });
39
43
  }
40
44
  );
@@ -0,0 +1,67 @@
1
+ import { test, expect } from "vitest";
2
+ import { quickbase } from "../../../../src/quickbaseClient";
3
+
4
+ test(
5
+ "QuickbaseClient Integration - getAppEvents > fetches real app events from QuickBase",
6
+ { timeout: 10000 },
7
+ async () => {
8
+ const config = {
9
+ realm: process.env.QB_REALM || "",
10
+ userToken: process.env.QB_USER_TOKEN || "",
11
+ debug: true,
12
+ };
13
+ const client = quickbase(config);
14
+ const appId = "buwai2zpe"; // Same app ID as getApp test
15
+
16
+ console.log("Config used:", config);
17
+ const result = await client.getAppEvents({ appId });
18
+ console.log("Real API response:", result);
19
+
20
+ // Validate the response is an array of events
21
+ expect(result).toBeInstanceOf(Array);
22
+
23
+ // Check that each event has the required properties
24
+ result.forEach((event) => {
25
+ expect(event).toHaveProperty("type");
26
+ expect([
27
+ "qb-action",
28
+ "webhook",
29
+ "email-notification",
30
+ "subscription",
31
+ "reminder",
32
+ "automation",
33
+ ]).toContain(event.type);
34
+
35
+ expect(event).toHaveProperty("owner");
36
+ expect(event.owner).toHaveProperty("email");
37
+ expect(typeof event.owner.email).toBe("string");
38
+ expect(event.owner).toHaveProperty("id");
39
+ expect(typeof event.owner.id).toBe("string");
40
+ expect(event.owner).toHaveProperty("name");
41
+ expect(typeof event.owner.name).toBe("string");
42
+ // userName is optional, so we don't enforce it
43
+
44
+ expect(event).toHaveProperty("isActive");
45
+ expect(typeof event.isActive).toBe("boolean");
46
+
47
+ expect(event).toHaveProperty("tableId");
48
+ expect(typeof event.tableId).toBe("string");
49
+
50
+ // Name is present for all types except automation
51
+ if (event.type !== "automation") {
52
+ expect(event).toHaveProperty("name");
53
+ expect(typeof event.name).toBe("string");
54
+ } else {
55
+ expect(event.name).toBeUndefined();
56
+ }
57
+
58
+ // URL is only present for automation
59
+ if (event.type === "automation") {
60
+ expect(event).toHaveProperty("url");
61
+ expect(typeof event.url).toBe("string");
62
+ } else {
63
+ expect(event.url).toBeUndefined();
64
+ }
65
+ });
66
+ }
67
+ );