quickbase-js 0.1.0-beta.8 → 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 (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +822 -119
  4. package/dist/esm/quickbase.js +373 -236
  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 +373 -236
  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/generate-unified-interface.ts +97 -17
  13. package/open-api/utils/generateJsDoc.ts +96 -0
  14. package/package.json +3 -1
  15. package/src/ThrottleBucket.ts +17 -12
  16. package/src/authorizationStrategy.ts +246 -0
  17. package/src/generated-unified/QuickbaseClient.ts +809 -119
  18. package/src/invokeMethod.ts +150 -265
  19. package/src/quickbaseClient.ts +85 -58
  20. package/src/rateLimiter.ts +25 -0
  21. package/src/tokenCache.ts +10 -26
  22. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  23. package/tests/vitest/qb/auth/retryUserToken401.test.ts +59 -42
  24. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  25. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  26. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  27. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  28. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  29. package/tests/vitest/qb/tables/updateRelationship.test.ts +118 -145
  30. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  31. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  32. package/tests/vitest/unit/apps/updateApp.test.ts +42 -11
  33. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +3 -2
  34. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  35. package/tests/vitest/unit/auth/retryTempToken401.test.ts +46 -64
  36. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  37. package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -43
  38. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  39. package/tests/vitest/unit/fields/createField.test.ts +180 -116
  40. package/tests/vitest/unit/fields/getFieldUsage.test.ts +17 -3
  41. package/tests/vitest/unit/fields/updateField.test.ts +12 -7
  42. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  43. package/tests/vitest/unit/index.ts +2 -1
  44. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  45. package/tests/vitest/unit/rateLimit.test.ts +4 -2
  46. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  47. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  48. package/tests/vitest/unit/tables/updateRelationship.test.ts +12 -10
  49. package/tests/vitest/unit/tokenLifespan.test.ts +149 -83
  50. package/tools/log-gen.ts +14 -5
  51. package/tools/log-gen.yaml +710 -268
  52. package/tests/vitest/unit/auth/retryTempTokenTwo401.test.ts +0 -81
@@ -1,8 +1,10 @@
1
1
  // src/invokeMethod.ts
2
- import { QuickbaseClient } from "./quickbaseClient";
3
- import { ResponseError } from "./generated/runtime";
4
- import { ThrottleBucket } from "./ThrottleBucket";
2
+
3
+ import { AuthorizationStrategy, extractDbid } from "./authorizationStrategy";
4
+ import { RateLimiter } from "./rateLimiter";
5
5
  import { RateLimitError } from "./RateLimitError";
6
+ import { ResponseError } from "./generated/runtime";
7
+ import { QuickbaseClient } from "./quickbaseClient";
6
8
  import { GetTempTokenDBID200Response } from "./generated/models";
7
9
 
8
10
  export type ApiMethod<K extends keyof QuickbaseClient> = (
@@ -17,92 +19,73 @@ export interface MethodInfo<K extends keyof QuickbaseClient> {
17
19
  httpMethod: string;
18
20
  }
19
21
 
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
22
  export async function invokeMethod<K extends keyof QuickbaseClient>(
32
23
  methodName: K,
33
- params: Parameters<QuickbaseClient[K]>[0] & Partial<TempTokenParams>,
24
+ params: Parameters<QuickbaseClient[K]>[0] & {
25
+ dbid?: string;
26
+ tableId?: string;
27
+ appId?: string;
28
+ },
34
29
  methodMap: { [P in keyof QuickbaseClient]: MethodInfo<P> },
35
30
  baseHeaders: Record<string, string>,
36
- tokenCache: TokenCache,
31
+ authStrategy: AuthorizationStrategy,
32
+ rateLimiter: RateLimiter,
37
33
  fetchTempToken: (dbid: string) => Promise<string>,
38
34
  transformDates: (obj: any, convertStringsToDates: boolean) => any,
39
- initialTempToken: string | undefined,
40
- userToken: string | undefined,
41
- useTempTokens: boolean | undefined,
42
35
  debug: boolean | undefined,
43
- convertDates: boolean,
44
- retryCount: number = 0,
45
- throttleBucket: ThrottleBucket | null = null,
46
- maxRetries: number = 3,
47
- retryDelay: number = 1000
36
+ convertDates: boolean
48
37
  ): Promise<ReturnType<QuickbaseClient[K]>> {
38
+ console.log("[invokeMethod] Starting for method:", methodName);
39
+
49
40
  const methodInfo = methodMap[methodName];
50
41
  if (!methodInfo) throw new Error(`Method ${methodName} not found`);
51
42
 
52
43
  const hasBody = "body" in params && params.body !== undefined;
53
- const body = hasBody ? (params as any).body : undefined;
44
+ const body = hasBody ? params.body : undefined;
54
45
  const restParams: any = hasBody
55
46
  ? Object.fromEntries(
56
47
  Object.entries(params).filter(([key]) => key !== "body")
57
48
  )
58
49
  : { ...params };
59
-
60
50
  const requestParameters: any = {
61
51
  ...restParams,
62
52
  ...(hasBody ? { generated: body } : {}),
63
53
  };
64
54
 
65
- let requestOptions: RequestInit = {
66
- credentials: "omit",
55
+ const requestOptions: RequestInit = {
56
+ credentials: methodName === "getTempTokenDBID" ? "include" : "omit",
67
57
  method: methodInfo.httpMethod,
68
- ...(hasBody ? { body } : {}),
69
58
  };
70
59
 
71
- const selectedToken =
72
- initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
73
- let authorizationToken = selectedToken;
60
+ let dbid: string | undefined = extractDbid(params);
61
+ console.log("[invokeMethod] Extracted dbid:", dbid);
74
62
 
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
- }
63
+ let token = dbid
64
+ ? await authStrategy.getToken(dbid)
65
+ : await authStrategy.getToken("");
66
+ console.log("[invokeMethod] Initial token:", token);
67
+
68
+ if (!token && dbid) {
69
+ token = await fetchTempToken(dbid);
70
+ authStrategy.applyHeaders(baseHeaders, token);
71
+ requestOptions.headers = { ...baseHeaders };
72
+ console.log("[invokeMethod] Fetched new token:", token);
83
73
  }
84
74
 
85
- if (useTempTokens && !authorizationToken) {
86
- const dbid = extractDbid(
87
- params,
88
- `No dbid found in params for ${methodName} to fetch temp token`
75
+ if (token) {
76
+ authStrategy.applyHeaders(baseHeaders, token);
77
+ requestOptions.headers = { ...baseHeaders };
78
+ console.log(
79
+ "[invokeMethod] Token found, headers set:",
80
+ requestOptions.headers
89
81
  );
90
- const cachedToken = tokenCache.get(dbid);
91
- authorizationToken = cachedToken || (await fetchTempToken(dbid));
92
82
  if (methodName === "getTempTokenDBID") {
93
- return Promise.resolve({
94
- temporaryAuthorization: authorizationToken,
95
- } as GetTempTokenDBID200Response as ReturnType<QuickbaseClient[K]>);
83
+ console.log("[invokeMethod] Using cached token:", token);
84
+ return { temporaryAuthorization: token } as ReturnType<
85
+ QuickbaseClient[K]
86
+ >;
96
87
  }
97
88
  }
98
- requestOptions.headers = authorizationToken
99
- ? {
100
- ...baseHeaders,
101
- Authorization: useTempTokens
102
- ? `QB-TEMP-TOKEN ${authorizationToken}`
103
- : `QB-USER-TOKEN ${authorizationToken}`,
104
- }
105
- : baseHeaders;
106
89
 
107
90
  if (debug) {
108
91
  console.log(`[${methodName}] requestParameters:`, requestParameters);
@@ -113,24 +96,11 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
113
96
  rawResponse: any
114
97
  ): Promise<ReturnType<QuickbaseClient[K]>> {
115
98
  if (debug) console.log(`[${methodName}] rawResponse:`, rawResponse);
116
-
117
99
  if (rawResponse instanceof Response) {
118
100
  const contentType = rawResponse.headers
119
101
  .get("Content-Type")
120
102
  ?.toLowerCase();
121
103
  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
104
  if (contentType?.includes("application/json")) {
135
105
  const jsonResponse = await rawResponse.json();
136
106
  return transformDates(jsonResponse, convertDates) as ReturnType<
@@ -139,7 +109,6 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
139
109
  }
140
110
  return rawResponse as ReturnType<QuickbaseClient[K]>;
141
111
  }
142
-
143
112
  if (rawResponse && typeof rawResponse.value === "function") {
144
113
  const response = await rawResponse.value();
145
114
  if (debug)
@@ -148,210 +117,126 @@ export async function invokeMethod<K extends keyof QuickbaseClient>(
148
117
  QuickbaseClient[K]
149
118
  >;
150
119
  }
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]>;
120
+ return transformDates(rawResponse, convertDates) as ReturnType<
121
+ QuickbaseClient[K]
122
+ >;
156
123
  }
157
124
 
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++;
125
+ async function parseErrorResponse(
126
+ response: Response
127
+ ): Promise<{ message: string; status: number }> {
128
+ let message = "Unknown error";
129
+ let status = response.status || 500;
130
+ try {
131
+ const contentType =
132
+ response.headers?.get("Content-Type")?.toLowerCase() ||
133
+ "application/json";
134
+ if (
135
+ contentType.includes("application/json") &&
136
+ typeof response.json === "function"
137
+ ) {
138
+ const errorBody = await response.json();
205
139
  if (
206
- !options.shouldRetry(effectiveError) ||
207
- attempt >= options.maxAttempts
140
+ errorBody &&
141
+ typeof errorBody === "object" &&
142
+ "message" in errorBody
208
143
  ) {
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);
144
+ message = errorBody.message;
145
+ } else {
146
+ message = "Invalid error response format";
236
147
  }
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));
148
+ } else if (typeof response.text === "function") {
149
+ message = (await response.text()) || message;
247
150
  }
151
+ } catch (e) {
152
+ if (debug) console.log(`[${methodName}] Error parsing response body:`, e);
153
+ message = "Failed to parse error response";
248
154
  }
155
+ console.log(
156
+ "[invokeMethod] Parsed error - status:",
157
+ status,
158
+ "message:",
159
+ message
160
+ );
161
+ return { message, status };
249
162
  }
250
163
 
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
- );
164
+ let attempt = 0;
165
+ const maxAttempts = rateLimiter.maxRetries + 1;
166
+
167
+ while (attempt < maxAttempts) {
168
+ console.log("[invokeMethod] Attempt:", attempt + 1, "of", maxAttempts);
169
+ try {
170
+ await rateLimiter.throttle();
171
+ console.log("[invokeMethod] About to call API for method:", methodName);
172
+ const response = await methodInfo.method(
173
+ requestParameters,
174
+ requestOptions
175
+ );
176
+ console.log("[invokeMethod] API call completed for method:", methodName);
177
+ return await processResponse(response);
178
+ } catch (error: unknown) {
179
+ let status: number;
180
+ let message: string;
181
+ let response: Response;
182
+
183
+ console.log("[invokeMethod] Caught error:", error);
184
+
185
+ if (error instanceof ResponseError && error.response) {
186
+ response = error.response;
187
+ ({ message, status } = await parseErrorResponse(response));
188
+ } else if (error instanceof Response) {
189
+ response = error;
190
+ ({ message, status } = await parseErrorResponse(response));
191
+ } else {
192
+ if (debug) console.log(`[${methodName}] Unexpected error:`, error);
193
+ throw error; // Rethrow non-response errors
194
+ }
313
195
 
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
- }
196
+ console.log("[invokeMethod] Handling error with status:", status);
325
197
 
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
- }
198
+ if (status === 429) {
199
+ if (!(error instanceof ResponseError)) {
200
+ throw new Error("Expected ResponseError for 429 handling"); // Shouldn't happen
201
+ }
202
+ const delay = await rateLimiter.handle429(error, attempt + 1);
203
+ if (debug) console.log(`[${methodName}] 429 delay: ${delay}ms`);
204
+ if (attempt + 1 === maxAttempts) {
205
+ throw new RateLimitError(
206
+ `API Error: ${message} (Status: ${status})`,
207
+ status,
208
+ response.headers.get("Retry-After")
209
+ ? parseInt(response.headers.get("Retry-After")!, 10)
210
+ : undefined
211
+ );
212
+ }
213
+ await new Promise((resolve) => setTimeout(resolve, delay));
214
+ attempt++;
215
+ continue;
216
+ }
349
217
 
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;
218
+ const newToken = await authStrategy.handleError(
219
+ status,
220
+ params,
221
+ fetchTempToken,
222
+ attempt,
223
+ maxAttempts,
224
+ debug,
225
+ methodName
226
+ );
227
+ if (newToken) {
228
+ token = newToken;
229
+ authStrategy.applyHeaders(baseHeaders, token);
230
+ requestOptions.headers = { ...baseHeaders };
231
+ attempt++;
232
+ if (debug)
233
+ console.log(
234
+ `[${methodName}] Retrying with token: ${token.substring(0, 10)}...`
235
+ );
236
+ continue;
237
+ }
238
+ throw new Error(`API Error: ${message} (Status: ${status})`);
239
+ }
240
+ }
241
+ throw new Error(`API Error: Exhausted retries after ${maxAttempts} attempts`);
357
242
  }