quickbase-js 0.1.0-beta.12 → 0.1.0-beta.14

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 (168) hide show
  1. package/README.md +48 -18
  2. package/dist/esm/quickbase.d.ts +13 -3
  3. package/dist/esm/quickbase.js +416 -49
  4. package/dist/esm/quickbase.js.map +1 -1
  5. package/dist/esm/quickbase.min.js +1 -1
  6. package/dist/esm/quickbase.min.js.map +1 -1
  7. package/dist/umd/quickbase.umd.js +416 -49
  8. package/dist/umd/quickbase.umd.js.map +1 -1
  9. package/dist/umd/quickbase.umd.min.js +1 -1
  10. package/dist/umd/quickbase.umd.min.js.map +1 -1
  11. package/package.json +11 -4
  12. package/CHANGELOG.md +0 -227
  13. package/build-common.js +0 -131
  14. package/build-esm.js +0 -71
  15. package/build-umd.js +0 -69
  16. package/docs/.vitepress/cache/deps/@theme_index.js +0 -275
  17. package/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
  18. package/docs/.vitepress/cache/deps/_metadata.json +0 -40
  19. package/docs/.vitepress/cache/deps/chunk-5A67QEFC.js +0 -12542
  20. package/docs/.vitepress/cache/deps/chunk-5A67QEFC.js.map +0 -7
  21. package/docs/.vitepress/cache/deps/chunk-7D4P4MMP.js +0 -9719
  22. package/docs/.vitepress/cache/deps/chunk-7D4P4MMP.js.map +0 -7
  23. package/docs/.vitepress/cache/deps/package.json +0 -3
  24. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4504
  25. package/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
  26. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
  27. package/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
  28. package/docs/.vitepress/cache/deps/vue.js +0 -343
  29. package/docs/.vitepress/cache/deps/vue.js.map +0 -7
  30. package/docs/.vitepress/config.mts +0 -26
  31. package/docs/api-examples.md +0 -49
  32. package/docs/index.md +0 -59
  33. package/docs/markdown-examples.md +0 -85
  34. package/docs-data/api-docs.json +0 -4143
  35. package/examples/quicbase-jsCDN.html +0 -36
  36. package/examples/react.js +0 -48
  37. package/examples/vanillajs.html +0 -47
  38. package/index.md +0 -25
  39. package/open-api/fix-spec-main.ts +0 -133
  40. package/open-api/generate-unified-interface.ts +0 -104
  41. package/open-api/generateDocsJson.ts +0 -24
  42. package/open-api/output/quickbase-fixed.json +0 -11191
  43. package/open-api/post-process-generated.ts +0 -226
  44. package/open-api/regenerate-client.ts +0 -117
  45. package/open-api/schema/enhance-general.ts +0 -111
  46. package/open-api/schema/fix-arrays.ts +0 -68
  47. package/open-api/schema/tags/apps.ts +0 -90
  48. package/open-api/schema/tags/fields.ts +0 -471
  49. package/open-api/schema/tags/groups.ts +0 -51
  50. package/open-api/schema/tags/index.ts +0 -66
  51. package/open-api/schema/tags/records.ts +0 -115
  52. package/open-api/schema/tags/tables.ts +0 -211
  53. package/open-api/schema/tags/users.ts +0 -47
  54. package/open-api/types/spec.ts +0 -42
  55. package/open-api/utils/common.ts +0 -29
  56. package/open-api/utils/generateJsDoc.ts +0 -74
  57. package/open-api/utils/infer-schema.ts +0 -40
  58. package/open-api/utils/naming.ts +0 -10
  59. package/open-api/utils/sharedUtils.ts +0 -352
  60. package/playwright.config.ts +0 -37
  61. package/rollup.config.js +0 -114
  62. package/specs/QuickBase_RESTful_API_2025-03-04T06_22_39.725Z.json +0 -46878
  63. package/specs/split/apps.json +0 -1315
  64. package/specs/split/auth.json +0 -191
  65. package/specs/split/fields.json +0 -3869
  66. package/specs/split/records.json +0 -754
  67. package/specs/split/reports.json +0 -619
  68. package/specs/split/tables.json +0 -1605
  69. package/src/RateLimitError.ts +0 -12
  70. package/src/ThrottleBucket.ts +0 -77
  71. package/src/authorizationStrategy.ts +0 -310
  72. package/src/generated-unified/QuickbaseClient.ts +0 -1135
  73. package/src/generated-unified/missing-types-report.json +0 -3
  74. package/src/index.ts +0 -1
  75. package/src/invokeMethod.ts +0 -212
  76. package/src/quickbaseClient.ts +0 -199
  77. package/src/rateLimiter.ts +0 -30
  78. package/src/tokenCache.ts +0 -39
  79. package/src/utils.ts +0 -50
  80. package/tests/playwright/qb/auth/cacheSwitching.test.ts +0 -159
  81. package/tests/playwright/qb/auth/fetchTempToken401.test.ts +0 -219
  82. package/tests/playwright/qb/auth/getAppWithTempToken.test.ts +0 -162
  83. package/tests/playwright/qb/auth/getAppWithTempTokenConcurrency.test.ts +0 -208
  84. package/tests/playwright/qb/auth/getAppWithUserToken.test.ts +0 -153
  85. package/tests/playwright/qb/auth/getTempToken.test.ts +0 -152
  86. package/tests/playwright/qb/auth/tempTokenPrefetch.test.ts +0 -179
  87. package/tests/playwright/qb/auth/tempTokenRenewal.test.ts +0 -133
  88. package/tests/setup.ts +0 -53
  89. package/tests/vitest/qb/ThrottleBucket.test.ts +0 -77
  90. package/tests/vitest/qb/apps/copyApp.test.ts +0 -76
  91. package/tests/vitest/qb/apps/createApp.test.ts +0 -64
  92. package/tests/vitest/qb/apps/deleteApp.test.ts +0 -76
  93. package/tests/vitest/qb/apps/getApp.test.ts +0 -44
  94. package/tests/vitest/qb/apps/getAppEvents.test.ts +0 -67
  95. package/tests/vitest/qb/apps/updateApp.test.ts +0 -58
  96. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +0 -60
  97. package/tests/vitest/qb/auth/retryUserToken401.test.ts +0 -73
  98. package/tests/vitest/qb/auth/ssoTokenConcurrency.test.ts +0 -68
  99. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +0 -65
  100. package/tests/vitest/qb/auth/userTokenConcurrency.test.ts +0 -63
  101. package/tests/vitest/qb/fields/createField.test.ts +0 -77
  102. package/tests/vitest/qb/fields/deleteFields.test.ts +0 -83
  103. package/tests/vitest/qb/fields/getField.test.ts +0 -85
  104. package/tests/vitest/qb/fields/getFieldUsage.test.ts +0 -82
  105. package/tests/vitest/qb/fields/getFields.test.ts +0 -232
  106. package/tests/vitest/qb/fields/getFieldsUsage.test.ts +0 -77
  107. package/tests/vitest/qb/fields/updateField.test.ts +0 -93
  108. package/tests/vitest/qb/formulas/runFormula.test.ts +0 -96
  109. package/tests/vitest/qb/index.ts +0 -29
  110. package/tests/vitest/qb/records/deleteRecords.test.ts +0 -110
  111. package/tests/vitest/qb/records/runQuery.test.ts +0 -146
  112. package/tests/vitest/qb/records/upsert.test.ts +0 -102
  113. package/tests/vitest/qb/tables/createRelationship.test.ts +0 -102
  114. package/tests/vitest/qb/tables/createTable.test.ts +0 -55
  115. package/tests/vitest/qb/tables/deleteRelationship.test.ts +0 -143
  116. package/tests/vitest/qb/tables/deleteTable.test.ts +0 -50
  117. package/tests/vitest/qb/tables/getAppTables.test.ts +0 -46
  118. package/tests/vitest/qb/tables/getRelationships.test.ts +0 -80
  119. package/tests/vitest/qb/tables/getTable.test.ts +0 -26
  120. package/tests/vitest/qb/tables/updateRelationship.test.ts +0 -250
  121. package/tests/vitest/qb/tables/updateTable.test.ts +0 -48
  122. package/tests/vitest/unit/ThrottleBucket.test.ts +0 -101
  123. package/tests/vitest/unit/apps/copyApp.test.ts +0 -581
  124. package/tests/vitest/unit/apps/createApp.test.ts +0 -186
  125. package/tests/vitest/unit/apps/deleteApp.test.ts +0 -216
  126. package/tests/vitest/unit/apps/getApp.test.ts +0 -128
  127. package/tests/vitest/unit/apps/getAppConcurrency.test.ts +0 -151
  128. package/tests/vitest/unit/apps/getAppEvents.test.ts +0 -193
  129. package/tests/vitest/unit/apps/updateApp.test.ts +0 -412
  130. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +0 -81
  131. package/tests/vitest/unit/auth/getTempToken.test.ts +0 -119
  132. package/tests/vitest/unit/auth/retryTempToken401.test.ts +0 -95
  133. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +0 -206
  134. package/tests/vitest/unit/auth/retryUserToken401.test.ts +0 -69
  135. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +0 -325
  136. package/tests/vitest/unit/auth/userTokenConcurrency.test.ts +0 -62
  137. package/tests/vitest/unit/fields/createField.test.ts +0 -451
  138. package/tests/vitest/unit/fields/deleteFields.test.ts +0 -141
  139. package/tests/vitest/unit/fields/getField.test.ts +0 -189
  140. package/tests/vitest/unit/fields/getFieldUsage.test.ts +0 -180
  141. package/tests/vitest/unit/fields/getFields.test.ts +0 -122
  142. package/tests/vitest/unit/fields/getFieldsUsage.test.ts +0 -160
  143. package/tests/vitest/unit/fields/updateField.test.ts +0 -285
  144. package/tests/vitest/unit/formulas/runFormula.test.ts +0 -205
  145. package/tests/vitest/unit/index.ts +0 -34
  146. package/tests/vitest/unit/quickbaseClientHttpMethods.test.ts +0 -139
  147. package/tests/vitest/unit/rateLimit.test.ts +0 -191
  148. package/tests/vitest/unit/records/deleteRecords.test.ts +0 -112
  149. package/tests/vitest/unit/records/runQuery.test.ts +0 -102
  150. package/tests/vitest/unit/records/upsert.test.ts +0 -234
  151. package/tests/vitest/unit/tables/createRelationship.test.ts +0 -212
  152. package/tests/vitest/unit/tables/createTable.test.ts +0 -70
  153. package/tests/vitest/unit/tables/deleteRelationship.test.ts +0 -97
  154. package/tests/vitest/unit/tables/deleteTable.test.ts +0 -77
  155. package/tests/vitest/unit/tables/getAppTables.test.ts +0 -85
  156. package/tests/vitest/unit/tables/getRelationships.test.ts +0 -259
  157. package/tests/vitest/unit/tables/getTable.test.ts +0 -233
  158. package/tests/vitest/unit/tables/updateRelationship.test.ts +0 -307
  159. package/tests/vitest/unit/tables/updateTable.test.ts +0 -80
  160. package/tests/vitest/unit/tokenLifespan.test.ts +0 -204
  161. package/tests/vitest/unit/umd.test.ts +0 -8
  162. package/tools/log-gen.ts +0 -251
  163. package/tools/log-gen.yaml +0 -4829
  164. package/tools/openapi-jars/openapi-generator-cli-7.12.0.jar +0 -0
  165. package/tsconfig.build.json +0 -17
  166. package/tsconfig.json +0 -32
  167. package/versionrc.json +0 -9
  168. package/vitest.config.ts +0 -27
@@ -7484,6 +7484,8 @@
7484
7484
  const newToken = await this.getToken(dbid);
7485
7485
  if (newToken) {
7486
7486
  this.tokenCache.set(dbid, newToken);
7487
+ if (debug)
7488
+ console.log(`[${methodName || "method"}] Retrying with token: ${newToken.substring(0, 10)}...`);
7487
7489
  return newToken;
7488
7490
  }
7489
7491
  return null;
@@ -7561,6 +7563,8 @@
7561
7563
  const newToken = await this.refreshSsoToken(debug || this.debug);
7562
7564
  if (newToken) {
7563
7565
  this.currentToken = newToken;
7566
+ if (debug || this.debug)
7567
+ console.log(`[${methodName || "method"}] Retrying with token: ${newToken.substring(0, 10)}...`);
7564
7568
  return newToken;
7565
7569
  }
7566
7570
  return null;
@@ -7646,20 +7650,318 @@
7646
7650
  }
7647
7651
  }
7648
7652
 
7649
- // src/invokeMethod.ts
7650
- async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, attempt = 0, maxAttempts = rateLimiter.maxRetries + 1) {
7653
+ /**
7654
+ * Paginates records from a Quick Base API endpoint, handling both skip-based and token-based pagination.
7655
+ * @template K - The method name keyof QuickbaseClient.
7656
+ * @param methodName - The Quick Base API method to invoke (e.g., "runQuery", "getUsers").
7657
+ * @param params - Parameters for the API method, including optional skip and top values.
7658
+ * @param methodMap - Mapping of method names to their implementation details.
7659
+ * @param baseHeaders - Base HTTP headers for API requests.
7660
+ * @param authStrategy - Strategy for handling authentication tokens.
7661
+ * @param rateLimiter - Rate limiting mechanism to respect API limits.
7662
+ * @param transformDates - Function to transform date strings in the response.
7663
+ * @param debug - If true, logs detailed pagination information to the console.
7664
+ * @param convertDates - If true, converts date strings to Date objects in the response.
7665
+ * @param initialResponse - Optional initial response to start pagination from (used by invokeMethod).
7666
+ * @returns A promise resolving to the complete paginated response with all records.
7667
+ * @throws {Error} If no response is received during pagination.
7668
+ */
7669
+ async function paginateRecords(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, initialResponse) {
7670
+ if (debug)
7671
+ console.log("[paginateRecords] Starting with params:", params);
7672
+ /** Extracts the key containing the data array from the response (e.g., "data" or "users"). */
7673
+ const getDataKey = (response) => Object.keys(response).find((key) => Array.isArray(response[key])) || "data";
7674
+ const dataKey = initialResponse ? getDataKey(initialResponse) : "data";
7675
+ let allRecords = initialResponse ? initialResponse[dataKey] : [];
7676
+ let lastResponse = initialResponse;
7677
+ let requestCount = 0;
7678
+ /**
7679
+ * Determines the pagination strategy based on response metadata.
7680
+ * @param response - The API response to analyze.
7681
+ * @returns "skip" for skip-based, "token" for token-based, or "none" if no pagination is detected.
7682
+ */
7683
+ const determinePaginationType = (response) => {
7684
+ if (response.metadata.totalRecords !== undefined &&
7685
+ response.metadata.skip !== undefined) {
7686
+ if (debug)
7687
+ console.log("[paginateRecords] Detected skip-based pagination");
7688
+ return "skip";
7689
+ }
7690
+ if (response.metadata.nextPageToken !== undefined ||
7691
+ response.metadata.nextToken !== undefined) {
7692
+ if (debug)
7693
+ console.log("[paginateRecords] Detected token-based pagination");
7694
+ return "token";
7695
+ }
7696
+ if (debug)
7697
+ console.log("[paginateRecords] No clear pagination type detected, assuming single-page response");
7698
+ return "none"; // No pagination indicators
7699
+ };
7700
+ let paginationType = initialResponse
7701
+ ? determinePaginationType(initialResponse)
7702
+ : "none"; // Start with no pagination assumption
7703
+ let continuation = initialResponse
7704
+ ? paginationType === "skip"
7705
+ ? {
7706
+ type: "skip",
7707
+ value: (initialResponse.metadata.skip ?? 0) +
7708
+ (initialResponse.metadata.numRecords || allRecords.length),
7709
+ }
7710
+ : paginationType === "token"
7711
+ ? {
7712
+ type: "token",
7713
+ value: initialResponse.metadata.nextPageToken ||
7714
+ initialResponse.metadata.nextToken ||
7715
+ "",
7716
+ key: initialResponse.metadata.nextPageToken !== undefined
7717
+ ? "nextPageToken"
7718
+ : "nextToken",
7719
+ }
7720
+ : null
7721
+ : null; // No continuation until type is determined
7722
+ if (debug) {
7723
+ console.log("[paginateRecords] Initial state:", {
7724
+ dataKey,
7725
+ paginationType,
7726
+ continuation,
7727
+ hasInitialResponse: !!initialResponse,
7728
+ totalRecordsSoFar: allRecords.length,
7729
+ });
7730
+ if (initialResponse && initialResponse[dataKey].length > 0) {
7731
+ console.log("[paginateRecords] Initial response IDs: first:", initialResponse[dataKey][0]["3"]?.value, "last:", initialResponse[dataKey][initialResponse[dataKey].length - 1]["3"]
7732
+ ?.value);
7733
+ }
7734
+ }
7735
+ /** Fetches the next page of records using the provided parameters. */
7736
+ const fetchNextPage = async (paginatedParams) => {
7737
+ if (debug)
7738
+ console.log("[paginateRecords] Fetching with params:", paginatedParams);
7739
+ const response = await invokeMethod(methodName, paginatedParams, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, false, 0, rateLimiter.maxRetries + 1, true);
7740
+ requestCount++;
7741
+ return response;
7742
+ };
7743
+ /** Constructs the parameters for the next API request based on the pagination type. */
7744
+ const getPaginatedParams = () => {
7745
+ const hasBody = "body" in params && params.body !== undefined;
7746
+ if (paginationType === "skip" && continuation?.type === "skip") {
7747
+ const paginatedOptions = {
7748
+ ...(hasBody && "options" in params.body ? params.body.options : {}),
7749
+ skip: continuation.value,
7750
+ };
7751
+ return hasBody
7752
+ ? {
7753
+ ...params,
7754
+ body: {
7755
+ ...params.body,
7756
+ options: paginatedOptions,
7757
+ },
7758
+ }
7759
+ : { ...params, skip: continuation.value };
7760
+ }
7761
+ else if (paginationType === "token" && continuation?.type === "token") {
7762
+ if (continuation.value === "" && !lastResponse) {
7763
+ return { ...params }; // Initial call, no pagination fields
7764
+ }
7765
+ const tokenKey = continuation.key;
7766
+ return hasBody
7767
+ ? {
7768
+ ...params,
7769
+ body: {
7770
+ ...params.body,
7771
+ [tokenKey]: continuation.value,
7772
+ },
7773
+ }
7774
+ : { ...params, [tokenKey]: continuation.value };
7775
+ }
7776
+ return params; // No pagination or initial call
7777
+ };
7778
+ if (!initialResponse) {
7779
+ const firstParams = getPaginatedParams();
7780
+ const firstResponse = await fetchNextPage(firstParams);
7781
+ const actualDataKey = getDataKey(firstResponse);
7782
+ allRecords = firstResponse[actualDataKey];
7783
+ lastResponse = firstResponse;
7784
+ paginationType = determinePaginationType(firstResponse);
7785
+ continuation =
7786
+ paginationType === "skip"
7787
+ ? {
7788
+ type: "skip",
7789
+ value: (firstResponse.metadata.skip ?? 0) +
7790
+ (firstResponse.metadata.numRecords || allRecords.length),
7791
+ }
7792
+ : paginationType === "token"
7793
+ ? {
7794
+ type: "token",
7795
+ value: firstResponse.metadata.nextPageToken ??
7796
+ firstResponse.metadata.nextToken ??
7797
+ "",
7798
+ key: firstResponse.metadata.nextPageToken !== undefined
7799
+ ? "nextPageToken"
7800
+ : "nextToken",
7801
+ }
7802
+ : null;
7803
+ }
7804
+ while (true) {
7805
+ const hasMore = (paginationType === "skip" &&
7806
+ continuation?.type === "skip" &&
7807
+ lastResponse.metadata.totalRecords !== undefined &&
7808
+ continuation.value < lastResponse.metadata.totalRecords &&
7809
+ allRecords.length < lastResponse.metadata.totalRecords) ||
7810
+ (paginationType === "token" &&
7811
+ continuation?.type === "token" &&
7812
+ continuation.value !== "");
7813
+ if (!hasMore) {
7814
+ if (debug) {
7815
+ console.log("[paginateRecords] Stopping:", paginationType === "skip"
7816
+ ? "All skip-based records fetched or no more data"
7817
+ : paginationType === "token"
7818
+ ? "Token exhausted"
7819
+ : "No pagination required");
7820
+ }
7821
+ break;
7822
+ }
7823
+ const paginatedParams = getPaginatedParams();
7824
+ const response = await fetchNextPage(paginatedParams);
7825
+ const actualDataKey = getDataKey(response);
7826
+ allRecords = allRecords.concat(response[actualDataKey]);
7827
+ lastResponse = response;
7828
+ if (debug && response[actualDataKey].length > 0) {
7829
+ console.log("[paginateRecords] Fetched records this iteration:", response[actualDataKey].length, "IDs: first:", response[actualDataKey][0]["3"]?.value, "last:", response[actualDataKey][response[actualDataKey].length - 1]["3"]?.value, "Continuation after fetch:", continuation);
7830
+ }
7831
+ if (paginationType === "skip") {
7832
+ const newSkip = continuation?.type === "skip"
7833
+ ? continuation.value + response[actualDataKey].length
7834
+ : (response.metadata.skip ?? 0) + response[actualDataKey].length;
7835
+ continuation = { type: "skip", value: newSkip };
7836
+ if (response[actualDataKey].length === 0) {
7837
+ if (debug &&
7838
+ lastResponse.metadata.totalRecords &&
7839
+ newSkip < lastResponse.metadata.totalRecords) {
7840
+ console.warn("[paginateRecords] Warning: Empty response received but records remain", { skip: newSkip, totalRecords: lastResponse.metadata.totalRecords });
7841
+ }
7842
+ break;
7843
+ }
7844
+ }
7845
+ else if (paginationType === "token") {
7846
+ const newToken = response.metadata.nextPageToken ?? response.metadata.nextToken ?? "";
7847
+ continuation = {
7848
+ type: "token",
7849
+ value: newToken,
7850
+ key: response.metadata.nextPageToken !== undefined
7851
+ ? "nextPageToken"
7852
+ : "nextToken",
7853
+ };
7854
+ if (continuation.value === "") {
7855
+ break;
7856
+ }
7857
+ else if (response[actualDataKey].length === 0 && newToken !== "") {
7858
+ if (debug)
7859
+ console.warn("[paginateRecords] Warning: Empty response with non-empty token, possible API inconsistency", { token: newToken });
7860
+ }
7861
+ }
7862
+ }
7863
+ if (!lastResponse) {
7864
+ throw new Error("[paginateRecords] No response received during pagination");
7865
+ }
7866
+ const finalDataKey = getDataKey(lastResponse);
7867
+ const finalResponse = {
7868
+ [finalDataKey]: allRecords,
7869
+ fields: lastResponse.fields,
7870
+ metadata: {
7871
+ totalRecords: paginationType === "skip"
7872
+ ? lastResponse.metadata.totalRecords
7873
+ : undefined,
7874
+ numRecords: allRecords.length,
7875
+ numFields: lastResponse.metadata.numFields,
7876
+ skip: paginationType === "skip" ? 0 : undefined,
7877
+ top: paginationType === "skip" ? undefined : undefined,
7878
+ ...(paginationType === "token" &&
7879
+ continuation?.type === "token" &&
7880
+ continuation.key
7881
+ ? { [continuation.key]: "" }
7882
+ : {}),
7883
+ },
7884
+ };
7885
+ if (debug) {
7886
+ console.log("[paginateRecords] Final response summary:", {
7887
+ totalRecords: finalResponse.metadata.totalRecords,
7888
+ numRecords: finalResponse.metadata.numRecords,
7889
+ numFields: finalResponse.metadata.numFields,
7890
+ skip: finalResponse.metadata.skip,
7891
+ top: finalResponse.metadata.top,
7892
+ ...(paginationType === "token" &&
7893
+ continuation?.type === "token" &&
7894
+ continuation.key
7895
+ ? { [continuation.key]: finalResponse.metadata[continuation.key] }
7896
+ : {}),
7897
+ });
7898
+ console.log("[paginateRecords] Pagination stats:", {
7899
+ totalRequests: requestCount,
7900
+ });
7901
+ }
7902
+ return finalResponse;
7903
+ }
7904
+ /**
7905
+ * Checks if a response is paginatable based on its structure.
7906
+ * @param response - The response to check.
7907
+ * @returns True if the response has a data array and pagination metadata, false otherwise.
7908
+ */
7909
+ function isPaginatable(response) {
7910
+ return (response &&
7911
+ typeof response === "object" &&
7912
+ Object.keys(response).some((key) => Array.isArray(response[key])) &&
7913
+ "metadata" in response &&
7914
+ typeof response.metadata === "object" &&
7915
+ (("totalRecords" in response.metadata &&
7916
+ "numRecords" in response.metadata &&
7917
+ "skip" in response.metadata) ||
7918
+ "nextPageToken" in response.metadata ||
7919
+ "nextToken" in response.metadata));
7920
+ }
7921
+
7922
+ /**
7923
+ * Invokes a Quick Base API method with the specified parameters, handling authentication, rate limiting, and pagination.
7924
+ * @template K - The method name keyof QuickbaseClient.
7925
+ * @param methodName - The API method to invoke (e.g., "runQuery", "changesetSolution").
7926
+ * @param params - Parameters for the API method, potentially including skip, top, or a body.
7927
+ * @param methodMap - Mapping of method names to their implementation details.
7928
+ * @param baseHeaders - Base HTTP headers for API requests.
7929
+ * @param authStrategy - Strategy for handling authentication tokens.
7930
+ * @param rateLimiter - Rate limiting mechanism to respect API limits.
7931
+ * @param transformDates - Function to transform date strings in the response.
7932
+ * @param debug - If true, logs detailed invocation information.
7933
+ * @param convertDates - If true, converts date strings to Date objects in the response.
7934
+ * @param autoPaginate - If true, automatically paginates multi-page responses (default: true).
7935
+ * @param attempt - Current retry attempt (default: 0).
7936
+ * @param maxAttempts - Maximum number of retry attempts (default: rateLimiter.maxRetries + 1).
7937
+ * @param isPaginating - Indicates if this call is part of an ongoing pagination (default: false).
7938
+ * @returns A promise resolving to the API method's response.
7939
+ * @throws {Error} If the method is not found or retries are exhausted.
7940
+ */
7941
+ async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, autoPaginate = true, attempt = 0, maxAttempts = rateLimiter.maxRetries + 1, isPaginating = false) {
7651
7942
  const methodInfo = methodMap[methodName];
7652
7943
  if (!methodInfo)
7653
7944
  throw new Error(`Method ${methodName} not found`);
7654
7945
  const hasBody = "body" in params && params.body !== undefined;
7655
7946
  const body = hasBody ? params.body : undefined;
7947
+ // Safely check for options only if body is an object
7948
+ const options = hasBody && body && typeof body === "object" && "options" in body
7949
+ ? body.options
7950
+ : undefined;
7951
+ const adjustedParams = {
7952
+ ...params,
7953
+ skip: params.skip ?? (options && "skip" in options ? options.skip : undefined),
7954
+ top: params.top ?? (options && "top" in options ? options.top : undefined),
7955
+ };
7656
7956
  const restParams = hasBody
7657
- ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "body" && key !== "startTime"))
7658
- : { ...params, startTime: undefined };
7957
+ ? Object.fromEntries(Object.entries(adjustedParams).filter(([key]) => key !== "body" && key !== "startTime"))
7958
+ : { ...adjustedParams, startTime: undefined };
7659
7959
  const requestParameters = {
7660
7960
  ...restParams,
7661
7961
  ...(hasBody ? { generated: body } : {}),
7662
7962
  };
7963
+ if (debug)
7964
+ console.log("[invokeMethod] Adjusted params for pagination:", adjustedParams);
7663
7965
  const requestOptions = {
7664
7966
  credentials: methodName === "getTempTokenDBID" ? "include" : "omit",
7665
7967
  method: methodInfo.httpMethod,
@@ -7682,12 +7984,22 @@
7682
7984
  ?.toLowerCase();
7683
7985
  if (contentType?.includes("application/json")) {
7684
7986
  const jsonResponse = await rawResponse.json();
7987
+ if (autoPaginate && !isPaginating && isPaginatable(jsonResponse)) {
7988
+ if (debug)
7989
+ console.log("[invokeMethod] Entering pagination with params:", adjustedParams);
7990
+ return paginateRecords(methodName, adjustedParams, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, jsonResponse);
7991
+ }
7685
7992
  return transformDates(jsonResponse, convertDates);
7686
7993
  }
7687
7994
  return rawResponse;
7688
7995
  }
7689
7996
  if (rawResponse && typeof rawResponse.value === "function") {
7690
7997
  const response = await rawResponse.value();
7998
+ if (autoPaginate && !isPaginating && isPaginatable(response)) {
7999
+ if (debug)
8000
+ console.log("[invokeMethod] Entering pagination with params:", adjustedParams);
8001
+ return paginateRecords(methodName, adjustedParams, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, response);
8002
+ }
7691
8003
  return transformDates(response, convertDates);
7692
8004
  }
7693
8005
  return transformDates(rawResponse, convertDates);
@@ -7724,11 +8036,18 @@
7724
8036
  try {
7725
8037
  await rateLimiter.throttle();
7726
8038
  acquired = true;
7727
- const postThrottleTime = Date.now(); // Capture post-throttle time
8039
+ const postThrottleTime = Date.now();
7728
8040
  if (params.startTime !== undefined)
7729
- params.startTime = postThrottleTime; // Update startTime for testing
7730
- const responsePromise = methodInfo.method(requestParameters, requestOptions);
7731
- rateLimiter.release(); // Release slot immediately after starting fetch
8041
+ params.startTime = postThrottleTime;
8042
+ const headers = { ...baseHeaders };
8043
+ const finalRequest = { ...requestParameters, headers };
8044
+ if (debug)
8045
+ console.log("[invokeMethod] Sending request:", finalRequest);
8046
+ const responsePromise = methodInfo.method(finalRequest, {
8047
+ ...requestOptions,
8048
+ headers,
8049
+ });
8050
+ rateLimiter.release();
7732
8051
  const response = await responsePromise;
7733
8052
  return await processResponse(response);
7734
8053
  }
@@ -7774,8 +8093,6 @@
7774
8093
  token = newToken;
7775
8094
  authStrategy.applyHeaders(baseHeaders, token);
7776
8095
  requestOptions.headers = { ...baseHeaders };
7777
- if (debug)
7778
- console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
7779
8096
  attempt++;
7780
8097
  continue;
7781
8098
  }
@@ -7785,8 +8102,37 @@
7785
8102
  throw new Error(`API Error: Exhausted retries after ${maxAttempts} attempts`);
7786
8103
  }
7787
8104
 
7788
- // src/ThrottleBucket.ts
7789
- class ConcurrentThrottleBucket {
8105
+ // src/Semaphore.ts
8106
+ class Semaphore {
8107
+ permits;
8108
+ waiting = [];
8109
+ constructor(maxPermits) {
8110
+ this.permits = maxPermits;
8111
+ }
8112
+ async acquire() {
8113
+ if (this.permits > 0) {
8114
+ this.permits -= 1;
8115
+ return;
8116
+ }
8117
+ return new Promise((resolve, reject) => {
8118
+ this.waiting.push({ resolve, reject });
8119
+ });
8120
+ }
8121
+ release() {
8122
+ this.permits += 1;
8123
+ if (this.waiting.length > 0 && this.permits > 0) {
8124
+ const { resolve } = this.waiting.shift();
8125
+ this.permits -= 1;
8126
+ resolve();
8127
+ }
8128
+ }
8129
+ available() {
8130
+ return this.permits;
8131
+ }
8132
+ }
8133
+
8134
+ // src/FlowThrottleBucket.ts
8135
+ class FlowThrottleBucket {
7790
8136
  tokens;
7791
8137
  maxTokens;
7792
8138
  refillRate;
@@ -7826,31 +8172,46 @@
7826
8172
  this.semaphore.release();
7827
8173
  }
7828
8174
  }
7829
- class Semaphore {
7830
- permits;
7831
- waiting = [];
7832
- constructor(maxPermits) {
7833
- this.permits = maxPermits;
8175
+
8176
+ // src/BurstAwareThrottleBucket.ts
8177
+ class BurstAwareThrottleBucket {
8178
+ maxTokens;
8179
+ windowSeconds;
8180
+ requestTimestamps = [];
8181
+ semaphore;
8182
+ constructor(options) {
8183
+ this.maxTokens = options.maxTokens;
8184
+ this.windowSeconds = options.windowSeconds;
8185
+ this.semaphore = new Semaphore(this.maxTokens);
8186
+ }
8187
+ countRequestsInWindow() {
8188
+ const now = Date.now();
8189
+ this.requestTimestamps = this.requestTimestamps.filter((ts) => now - ts < this.windowSeconds * 1000);
8190
+ return this.requestTimestamps.length;
8191
+ }
8192
+ availableTokens() {
8193
+ const windowCount = this.countRequestsInWindow();
8194
+ return Math.max(0, Math.min(100 - windowCount, this.maxTokens - windowCount));
7834
8195
  }
7835
8196
  async acquire() {
7836
- if (this.permits > 0) {
7837
- this.permits -= 1;
7838
- return;
8197
+ while (true) {
8198
+ const available = this.availableTokens();
8199
+ if (available > 0 && this.semaphore.available() > 0) {
8200
+ await this.semaphore.acquire();
8201
+ this.requestTimestamps.push(Date.now());
8202
+ return;
8203
+ }
8204
+ const now = Date.now();
8205
+ const oldestTimestamp = this.requestTimestamps[0];
8206
+ let waitTime = 100;
8207
+ if (oldestTimestamp) {
8208
+ waitTime = Math.max(0, this.windowSeconds * 1000 - (now - oldestTimestamp));
8209
+ }
8210
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
7839
8211
  }
7840
- return new Promise((resolve, reject) => {
7841
- this.waiting.push({ resolve, reject });
7842
- });
7843
8212
  }
7844
8213
  release() {
7845
- this.permits += 1;
7846
- if (this.waiting.length > 0 && this.permits > 0) {
7847
- const { resolve } = this.waiting.shift();
7848
- this.permits -= 1;
7849
- resolve();
7850
- }
7851
- }
7852
- available() {
7853
- return this.permits;
8214
+ this.semaphore.release();
7854
8215
  }
7855
8216
  }
7856
8217
 
@@ -7858,8 +8219,7 @@
7858
8219
  throttleBucket;
7859
8220
  maxRetries;
7860
8221
  retryDelay;
7861
- constructor(throttleBucket, // Updated type
7862
- maxRetries = 3, retryDelay = 1000) {
8222
+ constructor(throttleBucket, maxRetries = 3, retryDelay = 1000) {
7863
8223
  this.throttleBucket = throttleBucket;
7864
8224
  this.maxRetries = maxRetries;
7865
8225
  this.retryDelay = retryDelay;
@@ -7883,20 +8243,23 @@
7883
8243
  }
7884
8244
 
7885
8245
  function quickbase(config) {
7886
- const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate: 5, burst: 3 }, // Updated default to match passing test
7887
- maxRetries = 3, retryDelay = 1000, tokenCache: providedTokenCache, baseUrl = "https://api.quickbase.com/v1", } = config;
8246
+ const { realm, userToken, tempToken, useTempTokens = false, useSso = false, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { type: "flow", rate: 5, burst: 50 }, // Changed rate from 6 to 5
8247
+ maxRetries = 3, retryDelay = 1000, tokenCache: providedTokenCache, baseUrl = "https://api.quickbase.com/v1", autoPaginate = true, } = config;
7888
8248
  const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
7889
- const throttleBucket = throttle
7890
- ? new ConcurrentThrottleBucket(throttle.rate, throttle.burst)
7891
- : null;
8249
+ const throttleOptions = throttle;
8250
+ const throttleBucket = throttleOptions.type === "burst-aware"
8251
+ ? new BurstAwareThrottleBucket({
8252
+ maxTokens: throttleOptions.burst || 100,
8253
+ windowSeconds: throttleOptions.windowSeconds || 10,
8254
+ })
8255
+ : new FlowThrottleBucket(throttleOptions.rate || 5, // Changed rate from 6 to 5
8256
+ throttleOptions.burst || 50);
7892
8257
  const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
7893
- const defaultFetch = typeof globalThis.window !== "undefined"
7894
- ? globalThis.window.fetch.bind(globalThis.window)
7895
- : undefined;
7896
- const effectiveFetch = fetchApi || defaultFetch;
7897
- if (!effectiveFetch) {
7898
- throw new Error("No fetch implementation available");
8258
+ const defaultFetch = globalThis.fetch || globalThis.window?.fetch?.bind(globalThis.window);
8259
+ if (!defaultFetch && !fetchApi) {
8260
+ throw new Error("No fetch implementation available. Please provide fetchApi in a Node.js environment without native fetch.");
7899
8261
  }
8262
+ const effectiveFetch = fetchApi || defaultFetch;
7900
8263
  const authStrategy = useSso
7901
8264
  ? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug, baseUrl)
7902
8265
  : useTempTokens
@@ -7951,19 +8314,23 @@
7951
8314
  const methodMap = buildMethodMap();
7952
8315
  const proxyHandler = {
7953
8316
  get: (_, prop) => {
7954
- console.log("[proxy] Accessing:", prop, "in methodMap:", prop in methodMap);
8317
+ if (debug) {
8318
+ console.log("[proxy] Accessing:", prop, "in methodMap:", prop in methodMap);
8319
+ }
7955
8320
  if (prop in methodMap) {
7956
8321
  const methodName = prop;
7957
- return (params) => invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates);
8322
+ return (params) => invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, autoPaginate);
8323
+ }
8324
+ if (debug) {
8325
+ console.log("[proxy] Method not found:", prop);
7958
8326
  }
7959
- console.log("[proxy] Method not found:", prop);
7960
8327
  return undefined;
7961
8328
  },
7962
8329
  };
7963
8330
  const proxy = new Proxy({}, proxyHandler);
7964
8331
  if (debug) {
7965
8332
  console.log("[createClient] Config:", config);
7966
- console.log("[createClient] Proxy created:", proxy);
8333
+ console.log("[createClient] Returning proxy");
7967
8334
  }
7968
8335
  return proxy;
7969
8336
  }