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
@@ -9645,40 +9645,27 @@ var apis = /*#__PURE__*/Object.freeze({
9645
9645
 
9646
9646
  class TokenCache {
9647
9647
  cache;
9648
- lifespan; // Token lifespan in milliseconds
9649
- constructor(lifespan = 4 * 60 * 1000 + 50 * 1000) {
9650
- // Default 4:50
9648
+ tempTokenLifespan;
9649
+ constructor(tempTokenLifespan = 4 * 60 * 1000 + 50 * 1000) {
9651
9650
  this.cache = new Map();
9652
- this.lifespan = lifespan;
9651
+ this.tempTokenLifespan = tempTokenLifespan;
9653
9652
  }
9654
9653
  get(dbid) {
9655
9654
  const entry = this.cache.get(dbid);
9656
9655
  const now = Date.now();
9657
- if (entry && entry.expiresAt > now) {
9656
+ if (entry && entry.expiresAt > now)
9658
9657
  return entry.token;
9659
- }
9660
- return undefined; // Expired or not found
9661
- }
9662
- // New method to get full entry
9663
- getEntry(dbid) {
9664
- const entry = this.cache.get(dbid);
9665
- const now = Date.now();
9666
- if (entry && entry.expiresAt > now) {
9667
- return entry;
9668
- }
9669
- return undefined; // Expired or not found
9658
+ if (entry)
9659
+ this.cache.delete(dbid); // Clean up expired temp tokens
9660
+ return undefined;
9670
9661
  }
9671
- set(dbid, token) {
9662
+ set(dbid, token, lifespan) {
9672
9663
  const now = Date.now();
9673
9664
  this.cache.set(dbid, {
9674
9665
  token,
9675
- expiresAt: now + this.lifespan,
9666
+ expiresAt: now + (lifespan || this.tempTokenLifespan),
9676
9667
  });
9677
9668
  }
9678
- dump() {
9679
- return Array.from(this.cache.entries());
9680
- }
9681
- // New method to clear the cache
9682
9669
  clear() {
9683
9670
  this.cache.clear();
9684
9671
  }
@@ -9692,7 +9679,180 @@ function simplifyName(name) {
9692
9679
  .replace(/^(\w)/, (_, c) => c.toLowerCase());
9693
9680
  }
9694
9681
 
9695
- async function invokeMethod(methodName, params, methodMap, baseHeaders, tokenCache, fetchTempToken, transformDates, initialTempToken, userToken, useTempTokens, debug, convertDates, retryCount = 0, throttleBucket = null, maxRetries = 3, retryDelay = 1000) {
9682
+ // src/authorizationStrategy.ts
9683
+ class TempTokenStrategy {
9684
+ tokenCache;
9685
+ initialTempToken;
9686
+ constructor(tokenCache, initialTempToken) {
9687
+ this.tokenCache = tokenCache;
9688
+ this.initialTempToken = initialTempToken;
9689
+ }
9690
+ async getToken(dbid) {
9691
+ return this.tokenCache.get(dbid) || this.initialTempToken;
9692
+ }
9693
+ applyHeaders(headers, token) {
9694
+ headers["Authorization"] = `QB-TEMP-TOKEN ${token}`;
9695
+ }
9696
+ async handleError(status, params, fetchTempToken, attempt, maxAttempts, debug, methodName) {
9697
+ if (status !== 401 || attempt >= maxAttempts - 1)
9698
+ return null;
9699
+ if (debug)
9700
+ console.log(`Authorization error for ${methodName || "method"} (temp token), refreshing token:`);
9701
+ const dbid = extractDbid(params);
9702
+ if (!dbid) {
9703
+ if (debug)
9704
+ console.log(`No dbid available for ${methodName || "method"}, skipping token refresh`);
9705
+ return null;
9706
+ }
9707
+ if (debug)
9708
+ console.log(`Refreshing temp token for dbid: ${dbid}`);
9709
+ const newToken = await fetchTempToken(dbid);
9710
+ this.tokenCache.set(dbid, newToken);
9711
+ return newToken;
9712
+ }
9713
+ }
9714
+ class UserTokenStrategy {
9715
+ userToken;
9716
+ constructor(userToken) {
9717
+ this.userToken = userToken;
9718
+ }
9719
+ async getToken(_dbid) {
9720
+ return this.userToken;
9721
+ }
9722
+ applyHeaders(headers, token) {
9723
+ headers["Authorization"] = `QB-USER-TOKEN ${token}`;
9724
+ }
9725
+ async handleError(status, _params, _fetchTempToken, attempt, maxAttempts, debug, methodName) {
9726
+ if (status !== 401 || attempt >= maxAttempts - 1)
9727
+ return null;
9728
+ if (debug)
9729
+ console.log(`Retrying ${methodName || "method"} with existing user token: ${this.userToken.substring(0, 10)}...`);
9730
+ return this.userToken;
9731
+ }
9732
+ }
9733
+ class SsoTokenStrategy {
9734
+ samlToken;
9735
+ realm;
9736
+ fetchApi;
9737
+ debug;
9738
+ currentToken;
9739
+ constructor(samlToken, realm, fetchApi, debug = false) {
9740
+ this.samlToken = samlToken;
9741
+ this.realm = realm;
9742
+ this.fetchApi = fetchApi;
9743
+ this.debug = debug;
9744
+ }
9745
+ async getToken(_dbid) {
9746
+ if (!this.currentToken) {
9747
+ this.currentToken = await this.fetchSsoToken({
9748
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
9749
+ requested_token_type: "urn:quickbase:params:oauth:token-type:temp_token",
9750
+ subject_token: this.samlToken,
9751
+ subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
9752
+ });
9753
+ }
9754
+ return this.currentToken;
9755
+ }
9756
+ applyHeaders(headers, token) {
9757
+ headers["Authorization"] = `QB-TEMP-TOKEN ${token}`;
9758
+ }
9759
+ async handleError(status, _params, _fetchTempToken, attempt, maxAttempts, debug, methodName) {
9760
+ if (status !== 401 || attempt >= maxAttempts - 1)
9761
+ return null;
9762
+ if (debug || this.debug) {
9763
+ console.log(`Authorization error for ${methodName || "method"} (SSO), refreshing token`);
9764
+ }
9765
+ const newToken = await this.refreshSsoToken(debug || this.debug);
9766
+ if (newToken) {
9767
+ this.currentToken = newToken;
9768
+ return newToken;
9769
+ }
9770
+ return null;
9771
+ }
9772
+ async refreshSsoToken(debug = false) {
9773
+ const payload = {
9774
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
9775
+ requested_token_type: "urn:quickbase:params:oauth:token-type:temp_token",
9776
+ subject_token: this.samlToken,
9777
+ subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
9778
+ };
9779
+ const response = await this.fetchApi(`https://api.quickbase.com/v1/auth/oauth/token`, {
9780
+ method: "POST",
9781
+ headers: {
9782
+ "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
9783
+ "Content-Type": "application/json",
9784
+ },
9785
+ body: JSON.stringify(payload),
9786
+ credentials: "omit",
9787
+ });
9788
+ if (!response.ok) {
9789
+ const errorBody = await response.json().catch(() => ({}));
9790
+ if (debug) {
9791
+ console.log(`[SSO Refresh] Failed: ${response.status}`, errorBody);
9792
+ }
9793
+ throw new Error(`SSO token refresh failed: ${errorBody.message || "Unknown error"}`);
9794
+ }
9795
+ const result = await response.json();
9796
+ const newToken = result.access_token;
9797
+ if (!newToken) {
9798
+ throw new Error("No access token returned from SSO token exchange");
9799
+ }
9800
+ if (debug) {
9801
+ console.log(`[SSO Refresh] New token: ${newToken.substring(0, 10)}...`);
9802
+ }
9803
+ return newToken;
9804
+ }
9805
+ async fetchSsoToken(params) {
9806
+ const response = await this.fetchApi(`https://api.quickbase.com/v1/auth/oauth/token`, {
9807
+ method: "POST",
9808
+ headers: {
9809
+ "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
9810
+ "Content-Type": "application/json",
9811
+ },
9812
+ body: JSON.stringify(params),
9813
+ credentials: "omit",
9814
+ });
9815
+ if (!response.ok) {
9816
+ const errorBody = await response.json().catch(() => ({}));
9817
+ if (this.debug) {
9818
+ console.log(`[fetchSsoToken] Failed: ${response.status}`, errorBody);
9819
+ }
9820
+ throw new Error(`SSO token fetch failed: ${errorBody.message || "Unknown error"}`);
9821
+ }
9822
+ const result = await response.json();
9823
+ const newToken = result.access_token;
9824
+ if (!newToken) {
9825
+ throw new Error("No access token returned from SSO token exchange");
9826
+ }
9827
+ if (this.debug) {
9828
+ console.log(`[fetchSsoToken] Fetched token: ${newToken.substring(0, 10)}...`);
9829
+ }
9830
+ return newToken;
9831
+ }
9832
+ }
9833
+ function extractDbid(params) {
9834
+ return (params.dbid ||
9835
+ params.tableId ||
9836
+ params.appId ||
9837
+ params.body?.from ||
9838
+ params.body?.to);
9839
+ }
9840
+
9841
+ // src/RateLimitError.ts
9842
+ class RateLimitError extends Error {
9843
+ status;
9844
+ retryAfter;
9845
+ constructor(message, status, retryAfter) {
9846
+ super(message);
9847
+ this.name = "RateLimitError";
9848
+ this.status = status;
9849
+ this.retryAfter = retryAfter; // Seconds from Retry-After header, if present
9850
+ }
9851
+ }
9852
+
9853
+ // src/invokeMethod.ts
9854
+ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, fetchTempToken, transformDates, debug, convertDates) {
9855
+ console.log("[invokeMethod] Starting for method:", methodName);
9696
9856
  const methodInfo = methodMap[methodName];
9697
9857
  if (!methodInfo)
9698
9858
  throw new Error(`Method ${methodName} not found`);
@@ -9705,40 +9865,31 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, tokenCac
9705
9865
  ...restParams,
9706
9866
  ...(hasBody ? { generated: body } : {}),
9707
9867
  };
9708
- let requestOptions = {
9709
- credentials: "omit",
9868
+ const requestOptions = {
9869
+ credentials: methodName === "getTempTokenDBID" ? "include" : "omit",
9710
9870
  method: methodInfo.httpMethod,
9711
- ...(hasBody ? { body } : {}),
9712
- };
9713
- const selectedToken = initialTempToken || (userToken && !useTempTokens ? userToken : undefined);
9714
- let authorizationToken = selectedToken;
9715
- if (methodName === "getTempTokenDBID" && useTempTokens) {
9716
- const dbid = extractDbid(params, "No dbid provided for getTempTokenDBID");
9717
- const cachedToken = tokenCache.get(dbid);
9718
- if (cachedToken) {
9719
- return Promise.resolve({
9720
- temporaryAuthorization: cachedToken,
9721
- });
9722
- }
9723
- }
9724
- if (useTempTokens && !authorizationToken) {
9725
- const dbid = extractDbid(params, `No dbid found in params for ${methodName} to fetch temp token`);
9726
- const cachedToken = tokenCache.get(dbid);
9727
- authorizationToken = cachedToken || (await fetchTempToken(dbid));
9871
+ };
9872
+ let dbid = extractDbid(params);
9873
+ console.log("[invokeMethod] Extracted dbid:", dbid);
9874
+ let token = dbid
9875
+ ? await authStrategy.getToken(dbid)
9876
+ : await authStrategy.getToken("");
9877
+ console.log("[invokeMethod] Initial token:", token);
9878
+ if (!token && dbid) {
9879
+ token = await fetchTempToken(dbid);
9880
+ authStrategy.applyHeaders(baseHeaders, token);
9881
+ requestOptions.headers = { ...baseHeaders };
9882
+ console.log("[invokeMethod] Fetched new token:", token);
9883
+ }
9884
+ if (token) {
9885
+ authStrategy.applyHeaders(baseHeaders, token);
9886
+ requestOptions.headers = { ...baseHeaders };
9887
+ console.log("[invokeMethod] Token found, headers set:", requestOptions.headers);
9728
9888
  if (methodName === "getTempTokenDBID") {
9729
- return Promise.resolve({
9730
- temporaryAuthorization: authorizationToken,
9731
- });
9889
+ console.log("[invokeMethod] Using cached token:", token);
9890
+ return { temporaryAuthorization: token };
9732
9891
  }
9733
9892
  }
9734
- requestOptions.headers = authorizationToken
9735
- ? {
9736
- ...baseHeaders,
9737
- Authorization: useTempTokens
9738
- ? `QB-TEMP-TOKEN ${authorizationToken}`
9739
- : `QB-USER-TOKEN ${authorizationToken}`,
9740
- }
9741
- : baseHeaders;
9742
9893
  if (debug) {
9743
9894
  console.log(`[${methodName}] requestParameters:`, requestParameters);
9744
9895
  console.log(`[${methodName}] requestOptions:`, requestOptions);
@@ -9752,13 +9903,6 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, tokenCac
9752
9903
  ?.toLowerCase();
9753
9904
  if (debug)
9754
9905
  console.log(`[${methodName}] contentType:`, contentType);
9755
- if (contentType?.includes("application/octet-stream")) {
9756
- return (await rawResponse.arrayBuffer());
9757
- }
9758
- if (contentType?.includes("application/x-yaml") ||
9759
- contentType?.includes("text/yaml")) {
9760
- return (await rawResponse.text());
9761
- }
9762
9906
  if (contentType?.includes("application/json")) {
9763
9907
  const jsonResponse = await rawResponse.json();
9764
9908
  return transformDates(jsonResponse, convertDates);
@@ -9771,164 +9915,113 @@ async function invokeMethod(methodName, params, methodMap, baseHeaders, tokenCac
9771
9915
  console.log(`[${methodName}] Resolved JSONApiResponse:`, response);
9772
9916
  return transformDates(response, convertDates);
9773
9917
  }
9774
- const transformed = transformDates(rawResponse, convertDates);
9775
- if (debug)
9776
- console.log(`[${methodName}] Transformed non-Response:`, transformed);
9777
- return transformed;
9778
- }
9779
- async function withRetries(fn, options) {
9780
- let attempt = 0;
9781
- while (true) {
9782
- try {
9783
- if (throttleBucket) {
9784
- if (debug)
9785
- console.log(`[${methodName}] Awaiting throttle bucket`);
9786
- await throttleBucket.acquire();
9787
- if (debug)
9788
- console.log(`[${methodName}] Throttle bucket acquired`);
9789
- }
9790
- return await fn();
9791
- }
9792
- catch (error) {
9793
- console.log(`[${methodName}] Caught error:`, error);
9794
- console.log(`[${methodName}] instanceof ResponseError:`, error instanceof ResponseError);
9795
- console.log(`[${methodName}] Response status:`, error.response?.status);
9796
- const effectiveError = error instanceof Object &&
9797
- "cause" in error &&
9798
- error.cause instanceof ResponseError
9799
- ? error.cause
9800
- : error instanceof ResponseError
9801
- ? error
9802
- : error;
9803
- console.log(`[${methodName}] Effective error:`, effectiveError);
9804
- console.log(`[${methodName}] Effective instanceof ResponseError:`, effectiveError instanceof ResponseError);
9805
- console.log(`[${methodName}] Effective response status:`, effectiveError.response?.status);
9806
- attempt++;
9807
- if (!options.shouldRetry(effectiveError) ||
9808
- attempt >= options.maxAttempts) {
9809
- let errorMessage = effectiveError instanceof ResponseError
9810
- ? effectiveError.message
9811
- : String(error);
9812
- console.log(`[${methodName}] No retry, throwing:`, errorMessage);
9813
- if (effectiveError instanceof ResponseError &&
9814
- effectiveError.response) {
9815
- try {
9816
- const errorBody = await effectiveError.response.json();
9817
- if (debug)
9818
- console.log(`[${methodName}] Error response body:`, errorBody);
9819
- errorMessage = errorBody?.message || errorMessage;
9820
- }
9821
- catch (e) {
9822
- if (debug)
9823
- console.log(`[${methodName}] Failed to parse error body:`, e);
9824
- }
9825
- throw new Error(`API Error: ${errorMessage} (Status: ${effectiveError.response.status})`);
9826
- }
9827
- throw new Error(`API Error: ${errorMessage} (Status: unknown)`);
9918
+ return transformDates(rawResponse, convertDates);
9919
+ }
9920
+ async function parseErrorResponse(response) {
9921
+ let message = "Unknown error";
9922
+ let status = response.status || 500;
9923
+ try {
9924
+ const contentType = response.headers?.get("Content-Type")?.toLowerCase() ||
9925
+ "application/json";
9926
+ if (contentType.includes("application/json") &&
9927
+ typeof response.json === "function") {
9928
+ const errorBody = await response.json();
9929
+ if (errorBody &&
9930
+ typeof errorBody === "object" &&
9931
+ "message" in errorBody) {
9932
+ message = errorBody.message;
9828
9933
  }
9829
- if (options.onRetry) {
9830
- console.log(`[${methodName}] Retrying, attempt:`, attempt);
9831
- await options.onRetry(effectiveError, attempt);
9934
+ else {
9935
+ message = "Invalid error response format";
9832
9936
  }
9833
- const delayMs = options.delay
9834
- ? options.delay(attempt, effectiveError)
9835
- : retryDelay * Math.pow(2, attempt - 1);
9836
- if (debug)
9837
- console.log(`[${methodName}] Retrying after ${delayMs}ms (attempt ${attempt}/${options.maxAttempts - 1})`);
9838
- await new Promise((resolve) => setTimeout(resolve, delayMs));
9839
9937
  }
9938
+ else if (typeof response.text === "function") {
9939
+ message = (await response.text()) || message;
9940
+ }
9941
+ }
9942
+ catch (e) {
9943
+ if (debug)
9944
+ console.log(`[${methodName}] Error parsing response body:`, e);
9945
+ message = "Failed to parse error response";
9840
9946
  }
9947
+ console.log("[invokeMethod] Parsed error - status:", status, "message:", message);
9948
+ return { message, status };
9841
9949
  }
9842
- const mainPromise = withRetries(() => methodInfo
9843
- .method(requestParameters, requestOptions)
9844
- .then(processResponse), {
9845
- maxAttempts: (useTempTokens ?? false) || userToken ? 2 : 1,
9846
- shouldRetry: (error) => error instanceof ResponseError &&
9847
- error.response?.status === 401 &&
9848
- ((useTempTokens ?? false) ||
9849
- (!!userToken && !(useTempTokens ?? false))),
9850
- onRetry: async (error) => {
9851
- if (error instanceof ResponseError && error.response?.status === 401) {
9852
- if (useTempTokens ?? false) {
9853
- if (debug)
9854
- console.log(`Authorization error for ${methodName} (temp token), refreshing token:`, error.message);
9855
- const dbid = extractDbid(params, `No dbid to refresh token after authorization error`);
9856
- try {
9857
- authorizationToken = await fetchTempToken(dbid);
9858
- }
9859
- catch (fetchError) {
9860
- const fetchErrorMessage = fetchError instanceof ResponseError && fetchError.response
9861
- ? (await fetchError.response.json())?.message ||
9862
- "Unauthorized"
9863
- : String(fetchError);
9864
- throw new Error(`API Error: ${fetchErrorMessage} (Status: ${fetchError instanceof ResponseError &&
9865
- fetchError.response?.status
9866
- ? fetchError.response.status
9867
- : "unknown"})`);
9868
- }
9869
- requestOptions.headers = {
9870
- ...baseHeaders,
9871
- Authorization: `QB-TEMP-TOKEN ${authorizationToken}`,
9872
- };
9950
+ let attempt = 0;
9951
+ const maxAttempts = rateLimiter.maxRetries + 1;
9952
+ while (attempt < maxAttempts) {
9953
+ console.log("[invokeMethod] Attempt:", attempt + 1, "of", maxAttempts);
9954
+ try {
9955
+ await rateLimiter.throttle();
9956
+ console.log("[invokeMethod] About to call API for method:", methodName);
9957
+ const response = await methodInfo.method(requestParameters, requestOptions);
9958
+ console.log("[invokeMethod] API call completed for method:", methodName);
9959
+ return await processResponse(response);
9960
+ }
9961
+ catch (error) {
9962
+ let status;
9963
+ let message;
9964
+ let response;
9965
+ console.log("[invokeMethod] Caught error:", error);
9966
+ if (error instanceof ResponseError && error.response) {
9967
+ response = error.response;
9968
+ ({ message, status } = await parseErrorResponse(response));
9969
+ }
9970
+ else if (error instanceof Response) {
9971
+ response = error;
9972
+ ({ message, status } = await parseErrorResponse(response));
9973
+ }
9974
+ else {
9975
+ if (debug)
9976
+ console.log(`[${methodName}] Unexpected error:`, error);
9977
+ throw error; // Rethrow non-response errors
9978
+ }
9979
+ console.log("[invokeMethod] Handling error with status:", status);
9980
+ if (status === 429) {
9981
+ if (!(error instanceof ResponseError)) {
9982
+ throw new Error("Expected ResponseError for 429 handling"); // Shouldn't happen
9873
9983
  }
9874
- else if (userToken) {
9875
- if (debug)
9876
- console.log(`Authorization error for ${methodName} (user token), retrying with same token:`, error.message);
9984
+ const delay = await rateLimiter.handle429(error, attempt + 1);
9985
+ if (debug)
9986
+ console.log(`[${methodName}] 429 delay: ${delay}ms`);
9987
+ if (attempt + 1 === maxAttempts) {
9988
+ throw new RateLimitError(`API Error: ${message} (Status: ${status})`, status, response.headers.get("Retry-After")
9989
+ ? parseInt(response.headers.get("Retry-After"), 10)
9990
+ : undefined);
9877
9991
  }
9992
+ await new Promise((resolve) => setTimeout(resolve, delay));
9993
+ attempt++;
9994
+ continue;
9995
+ }
9996
+ const newToken = await authStrategy.handleError(status, params, fetchTempToken, attempt, maxAttempts, debug, methodName);
9997
+ if (newToken) {
9998
+ token = newToken;
9999
+ authStrategy.applyHeaders(baseHeaders, token);
10000
+ requestOptions.headers = { ...baseHeaders };
10001
+ attempt++;
9878
10002
  if (debug)
9879
- console.log(`Retrying ${methodName} with ${useTempTokens ? "temp" : "user"} token`);
10003
+ console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
10004
+ continue;
9880
10005
  }
9881
- },
9882
- });
9883
- return mainPromise.catch((error) => {
9884
- let effectiveError = error;
9885
- if (error instanceof Object &&
9886
- "cause" in error &&
9887
- error.cause instanceof ResponseError) {
9888
- effectiveError = error.cause;
9889
- }
9890
- else if (error instanceof ResponseError) {
9891
- effectiveError = error;
9892
- }
9893
- if (effectiveError instanceof ResponseError &&
9894
- effectiveError.response?.status === 429) {
9895
- return withRetries(() => Promise.reject(error), {
9896
- maxAttempts: maxRetries + 1,
9897
- shouldRetry: (err) => err instanceof ResponseError && err.response?.status === 429,
9898
- delay: (attempt, err) => {
9899
- const headers = err instanceof ResponseError && err.response
9900
- ? err.response.headers
9901
- : new Headers();
9902
- const retryAfter = headers.get("Retry-After");
9903
- return retryAfter
9904
- ? parseInt(retryAfter, 10) * 1000
9905
- : retryDelay * Math.pow(2, attempt - 1);
9906
- },
9907
- });
10006
+ throw new Error(`API Error: ${message} (Status: ${status})`);
9908
10007
  }
9909
- throw effectiveError;
9910
- });
9911
- }
9912
- function extractDbid(params, errorMessage) {
9913
- const dbid = params.dbid || params.tableId || params.appId;
9914
- if (!dbid)
9915
- throw new Error(errorMessage);
9916
- return dbid;
10008
+ }
10009
+ throw new Error(`API Error: Exhausted retries after ${maxAttempts} attempts`);
9917
10010
  }
9918
10011
 
9919
- // src/TokenBucket.ts
10012
+ // src/ThrottleBucket.ts
9920
10013
  class ThrottleBucket {
9921
10014
  tokens;
9922
10015
  maxTokens;
9923
10016
  refillRate; // Tokens per second
9924
10017
  lastRefill;
10018
+ pending = Promise.resolve(); // Queue for sequential execution
9925
10019
  constructor(rate, burst) {
9926
- this.tokens = burst; // Start with full burst capacity
10020
+ this.tokens = burst;
9927
10021
  this.maxTokens = burst;
9928
10022
  this.refillRate = rate;
9929
10023
  this.lastRefill = Date.now();
9930
10024
  }
9931
- // Refill tokens based on elapsed time
9932
10025
  refill() {
9933
10026
  const now = Date.now();
9934
10027
  const elapsed = (now - this.lastRefill) / 1000; // Seconds elapsed
@@ -9936,20 +10029,49 @@ class ThrottleBucket {
9936
10029
  this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
9937
10030
  this.lastRefill = now;
9938
10031
  }
9939
- // Acquire a token, waiting if necessary
9940
10032
  async acquire() {
9941
- this.refill();
9942
- if (this.tokens >= 1) {
10033
+ // Chain the new acquisition onto the pending queue
10034
+ const previous = this.pending;
10035
+ this.pending = (async () => {
10036
+ await previous; // Wait for prior calls to complete
10037
+ this.refill();
10038
+ if (this.tokens >= 1) {
10039
+ this.tokens -= 1;
10040
+ return;
10041
+ }
10042
+ const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
10043
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
10044
+ this.refill();
9943
10045
  this.tokens -= 1;
9944
- return;
10046
+ })();
10047
+ await this.pending;
10048
+ }
10049
+ }
10050
+
10051
+ class RateLimiter {
10052
+ throttleBucket;
10053
+ maxRetries;
10054
+ retryDelay;
10055
+ constructor(throttleBucket, maxRetries = 3, retryDelay = 1000) {
10056
+ this.throttleBucket = throttleBucket;
10057
+ this.maxRetries = maxRetries;
10058
+ this.retryDelay = retryDelay;
10059
+ }
10060
+ async throttle() {
10061
+ if (this.throttleBucket) {
10062
+ await this.throttleBucket.acquire();
9945
10063
  }
9946
- const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
9947
- await new Promise((resolve) => setTimeout(resolve, waitTime));
9948
- this.refill();
9949
- this.tokens -= 1;
10064
+ }
10065
+ async handle429(error, attempt) {
10066
+ // Remove the throw here; let invokeMethod handle exhaustion
10067
+ const retryAfter = error.response.headers.get("Retry-After");
10068
+ return retryAfter
10069
+ ? parseInt(retryAfter, 10) * 1000
10070
+ : this.retryDelay * Math.pow(2, attempt - 1);
9950
10071
  }
9951
10072
  }
9952
10073
 
10074
+ // src/quickbaseClient.ts
9953
10075
  const getParamNames = (fn) => fn
9954
10076
  .toString()
9955
10077
  .slice(fn.toString().indexOf("(") + 1, fn.toString().indexOf(")"))
@@ -9978,46 +10100,58 @@ function transformDates(obj, convertStringsToDates = true) {
9978
10100
  return obj;
9979
10101
  }
9980
10102
  function inferHttpMethod(methodSource, debug) {
9981
- const methodMatch = methodSource.match(/method:\s*['"]?(\w+)['"]?/i);
9982
- const method = methodMatch ? methodMatch[1].toUpperCase() : "GET";
10103
+ const methodMatches = [
10104
+ ...methodSource.matchAll(/method:\s*['"]?(\w+)['"]?/gi),
10105
+ ];
10106
+ const method = methodMatches.length > 0
10107
+ ? methodMatches[methodMatches.length - 1][1].toUpperCase()
10108
+ : "GET";
10109
+ if (debug) {
10110
+ console.log(`[inferHttpMethod] Source:`, methodSource);
10111
+ console.log(`[inferHttpMethod] All matches:`, methodMatches.map((m) => m[0]));
10112
+ console.log(`[inferHttpMethod] Extracted method:`, method);
10113
+ }
9983
10114
  return method;
9984
10115
  }
9985
10116
  function quickbase(config) {
9986
- const { realm, userToken, tempToken: initialTempToken, useTempTokens, fetchApi, debug, convertDates = true, tokenLifespan, throttle = { rate: 10, burst: 10 }, maxRetries = 3, retryDelay = 1000, } = config;
9987
- const baseUrl = `https://api.quickbase.com/v1`;
9988
- const tokenCache = new TokenCache(tokenLifespan);
10117
+ const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate: 10, burst: 10 }, maxRetries = 3, retryDelay = 1000, tokenCache: providedTokenCache, baseUrl = "https://api.quickbase.com/v1", } = config;
10118
+ const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
9989
10119
  const throttleBucket = throttle
9990
10120
  ? new ThrottleBucket(throttle.rate, throttle.burst)
9991
10121
  : null;
10122
+ const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
10123
+ const defaultFetch = typeof globalThis.window !== "undefined"
10124
+ ? globalThis.window.fetch.bind(globalThis.window)
10125
+ : undefined;
10126
+ const effectiveFetch = fetchApi || defaultFetch;
10127
+ if (!effectiveFetch) {
10128
+ throw new Error("No fetch implementation available");
10129
+ }
10130
+ const authStrategy = useSso
10131
+ ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
10132
+ : useTempTokens
10133
+ ? new TempTokenStrategy(tokenCache, tempToken)
10134
+ : new UserTokenStrategy(userToken || "");
9992
10135
  const baseHeaders = {
9993
10136
  "QB-Realm-Hostname": `${realm}.quickbase.com`,
9994
10137
  "Content-Type": "application/json",
9995
10138
  };
9996
- if (initialTempToken) {
9997
- baseHeaders["Authorization"] = `QB-TEMP-TOKEN ${initialTempToken}`;
9998
- }
9999
- else if (userToken && !useTempTokens) {
10000
- baseHeaders["Authorization"] = `QB-USER-TOKEN ${userToken}`;
10001
- }
10002
- const defaultFetch = typeof globalThis.window !== "undefined"
10003
- ? globalThis.window.fetch.bind(globalThis.window)
10004
- : undefined;
10005
10139
  const configuration = new Configuration({
10006
10140
  basePath: baseUrl,
10007
10141
  headers: { ...baseHeaders },
10008
- fetchApi: fetchApi || defaultFetch,
10142
+ fetchApi: effectiveFetch,
10009
10143
  credentials: "omit",
10010
10144
  });
10011
- if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
10012
- throw new Error("fetchApi must be provided in non-browser environments (e.g., Node.js)");
10013
- }
10145
+ // Removed redundant check:
10146
+ // if (!configuration.fetchApi && typeof globalThis.window === "undefined") {
10147
+ // throw new Error("fetchApi must be provided in non-browser environments (e.g., Node.js)");
10148
+ // }
10014
10149
  const apiInstances = Object.fromEntries(Object.entries(apis)
10015
10150
  .filter(([name]) => name.endsWith("Api"))
10016
10151
  .map(([name, ApiClass]) => [
10017
10152
  name.replace("Api", "").toLowerCase(),
10018
10153
  new ApiClass(configuration),
10019
10154
  ]));
10020
- const methodMap = buildMethodMap();
10021
10155
  function buildMethodMap() {
10022
10156
  const methodMap = {};
10023
10157
  const isValidMethod = (name) => !name.startsWith("_") &&
@@ -10036,21 +10170,21 @@ function quickbase(config) {
10036
10170
  const methodSource = method.toString();
10037
10171
  methodMap[simplifiedName] = {
10038
10172
  api,
10039
- method: boundMethod, // Adjust typing as needed
10173
+ method: boundMethod,
10040
10174
  paramMap: getParamNames(method),
10041
- httpMethod: inferHttpMethod(methodSource),
10175
+ httpMethod: inferHttpMethod(methodSource, debug),
10042
10176
  };
10043
10177
  }
10044
10178
  });
10045
10179
  }
10180
+ if (debug) {
10181
+ console.log("[buildMethodMap] Methods:", Object.keys(methodMap));
10182
+ }
10046
10183
  return methodMap;
10047
10184
  }
10185
+ const methodMap = buildMethodMap();
10048
10186
  const fetchTempToken = async (dbid) => {
10049
- const effectiveFetch = fetchApi || defaultFetch;
10050
- if (!effectiveFetch) {
10051
- throw new Error("No fetch implementation available for fetching temp token");
10052
- }
10053
- const response = await effectiveFetch(`https://api.quickbase.com/v1/auth/temporary/${dbid}`, {
10187
+ const response = await effectiveFetch(`${baseUrl}/auth/temporary/${dbid}`, {
10054
10188
  method: "GET",
10055
10189
  headers: { ...baseHeaders },
10056
10190
  credentials: "include",
@@ -10060,29 +10194,32 @@ function quickbase(config) {
10060
10194
  throw new Error(`API Error: ${errorBody.message || "Unknown error"} (Status: ${response.status})`);
10061
10195
  }
10062
10196
  const tokenResult = await response.json();
10197
+ console.log(`[fetchTempToken] Response:`, tokenResult);
10063
10198
  const token = tokenResult.temporaryAuthorization;
10064
10199
  if (!token) {
10065
10200
  throw new Error("No temporary token returned from API");
10066
10201
  }
10067
- tokenCache.set(dbid, token);
10202
+ tokenCache.set(dbid, token, tempTokenLifespan);
10068
10203
  if (debug) {
10069
10204
  console.log(`Fetched and cached new token for dbid: ${dbid}`, token);
10070
10205
  }
10071
10206
  return token;
10072
10207
  };
10073
- const proxy = new Proxy({}, {
10208
+ const proxyHandler = {
10074
10209
  get: (_, prop) => {
10210
+ console.log("[proxy] Accessing:", prop, "in methodMap:", prop in methodMap);
10075
10211
  if (prop in methodMap) {
10076
10212
  const methodName = prop;
10077
- return (params) => invokeMethod(methodName, params, methodMap, baseHeaders, tokenCache, fetchTempToken, transformDates, initialTempToken, userToken, useTempTokens, debug, convertDates, 0, // retryCount
10078
- throttleBucket, maxRetries, retryDelay);
10213
+ return (params) => invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, fetchTempToken, transformDates, debug, convertDates);
10079
10214
  }
10215
+ console.log("[proxy] Method not found:", prop);
10080
10216
  return undefined;
10081
10217
  },
10082
- });
10218
+ };
10219
+ const proxy = new Proxy({}, proxyHandler);
10083
10220
  if (debug) {
10084
10221
  console.log("[createClient] Config:", config);
10085
- console.log("[createClient] Returning:", proxy);
10222
+ console.log("[createClient] Proxy created:", proxy);
10086
10223
  }
10087
10224
  return proxy;
10088
10225
  }