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