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