quickbase-js 0.1.0-beta.11 → 0.1.0-beta.13
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/CHANGELOG.md +19 -0
- package/README.md +40 -18
- package/dist/esm/quickbase.d.ts +12 -3
- package/dist/esm/quickbase.js +452 -43
- 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 +452 -43
- 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 +3 -1
- package/src/BurstAwareThrottleBucket.ts +57 -0
- package/src/FlowThrottleBucket.ts +47 -0
- package/src/RateThrottleBucket.ts +5 -0
- package/src/Semaphore.ts +33 -0
- package/src/authorizationStrategy.ts +12 -1
- package/src/generated-unified/QuickbaseClient.ts +1 -1
- package/src/invokeMethod.ts +102 -17
- package/src/pagination.ts +429 -0
- package/src/quickbaseClient.ts +56 -28
- package/src/rateLimiter.ts +9 -4
- package/tests/vitest/qb/fields/getFieldsUsage.test.ts +2 -2
- package/tests/vitest/qb/quickbase-js/BurstAwareThrottleBucket.test.ts +125 -0
- package/tests/vitest/qb/quickbase-js/FlowBucket.test.ts +77 -0
- package/tests/vitest/qb/quickbase-js/paginationSkip.test.ts +111 -0
- package/tests/vitest/qb/records/runQuery.test.ts +1 -2
- package/tests/vitest/unit/auth/retryUserToken401.test.ts +7 -10
- package/tests/vitest/unit/quickbase-js/BurstAwareThrottleBucket.test.ts +108 -0
- package/tests/vitest/unit/{ThrottleBucket.test.ts → quickbase-js/FlowBucket.test.ts} +16 -10
- package/tests/vitest/unit/quickbase-js/pagination.test.ts +251 -0
- package/tests/vitest/unit/{quickbaseClientHttpMethods.test.ts → quickbase-js/quickbaseClientHttpMethods.test.ts} +13 -11
- package/tests/vitest/unit/records/runQuery.test.ts +3 -4
- package/src/ThrottleBucket.ts +0 -41
- package/tests/vitest/unit/umd.test.ts +0 -8
- /package/tests/vitest/unit/{rateLimit.test.ts → quickbase-js/rateLimit.test.ts} +0 -0
- /package/tests/vitest/unit/{tokenLifespan.test.ts → quickbase-js/tokenLifespan.test.ts} +0 -0
|
@@ -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
|
-
|
|
7650
|
-
|
|
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(
|
|
7658
|
-
: { ...
|
|
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);
|
|
@@ -7720,12 +8032,28 @@
|
|
|
7720
8032
|
return { message, status };
|
|
7721
8033
|
}
|
|
7722
8034
|
while (attempt < maxAttempts) {
|
|
8035
|
+
let acquired = false;
|
|
7723
8036
|
try {
|
|
7724
8037
|
await rateLimiter.throttle();
|
|
7725
|
-
|
|
8038
|
+
acquired = true;
|
|
8039
|
+
const postThrottleTime = Date.now();
|
|
8040
|
+
if (params.startTime !== undefined)
|
|
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();
|
|
8051
|
+
const response = await responsePromise;
|
|
7726
8052
|
return await processResponse(response);
|
|
7727
8053
|
}
|
|
7728
8054
|
catch (error) {
|
|
8055
|
+
if (acquired)
|
|
8056
|
+
rateLimiter.release();
|
|
7729
8057
|
let status;
|
|
7730
8058
|
let message;
|
|
7731
8059
|
let response;
|
|
@@ -7738,7 +8066,7 @@
|
|
|
7738
8066
|
({ message, status } = await parseErrorResponse(response));
|
|
7739
8067
|
}
|
|
7740
8068
|
else {
|
|
7741
|
-
throw error;
|
|
8069
|
+
throw error;
|
|
7742
8070
|
}
|
|
7743
8071
|
if (status === 429) {
|
|
7744
8072
|
if (!(error instanceof ResponseError)) {
|
|
@@ -7754,67 +8082,136 @@
|
|
|
7754
8082
|
attempt++;
|
|
7755
8083
|
continue;
|
|
7756
8084
|
}
|
|
7757
|
-
// Handle authentication errors
|
|
7758
8085
|
let newToken;
|
|
7759
8086
|
try {
|
|
7760
8087
|
newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
|
|
7761
8088
|
}
|
|
7762
8089
|
catch (authError) {
|
|
7763
|
-
throw authError;
|
|
8090
|
+
throw authError;
|
|
7764
8091
|
}
|
|
7765
|
-
// Only proceed with retry if we have a valid new token
|
|
7766
8092
|
if (newToken !== null) {
|
|
7767
8093
|
token = newToken;
|
|
7768
8094
|
authStrategy.applyHeaders(baseHeaders, token);
|
|
7769
8095
|
requestOptions.headers = { ...baseHeaders };
|
|
7770
|
-
if (debug)
|
|
7771
|
-
console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
|
|
7772
8096
|
attempt++;
|
|
7773
8097
|
continue;
|
|
7774
8098
|
}
|
|
7775
|
-
// If no new token was provided, throw the original error
|
|
7776
8099
|
throw new Error(`API Error: ${message} (Status: ${status})`);
|
|
7777
8100
|
}
|
|
7778
8101
|
}
|
|
7779
8102
|
throw new Error(`API Error: Exhausted retries after ${maxAttempts} attempts`);
|
|
7780
8103
|
}
|
|
7781
8104
|
|
|
7782
|
-
// src/
|
|
7783
|
-
class
|
|
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 {
|
|
7784
8136
|
tokens;
|
|
7785
8137
|
maxTokens;
|
|
7786
|
-
refillRate;
|
|
8138
|
+
refillRate;
|
|
7787
8139
|
lastRefill;
|
|
7788
|
-
|
|
8140
|
+
semaphore;
|
|
7789
8141
|
constructor(rate, burst) {
|
|
7790
8142
|
this.tokens = burst;
|
|
7791
8143
|
this.maxTokens = burst;
|
|
7792
8144
|
this.refillRate = rate;
|
|
7793
8145
|
this.lastRefill = Date.now();
|
|
8146
|
+
this.semaphore = new Semaphore(burst);
|
|
7794
8147
|
}
|
|
7795
8148
|
refill() {
|
|
7796
8149
|
const now = Date.now();
|
|
7797
|
-
const elapsed = (now - this.lastRefill) / 1000;
|
|
8150
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
7798
8151
|
const newTokens = elapsed * this.refillRate;
|
|
7799
8152
|
this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
|
|
7800
8153
|
this.lastRefill = now;
|
|
7801
8154
|
}
|
|
7802
8155
|
async acquire() {
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
this.pending = (async () => {
|
|
7806
|
-
await previous; // Wait for prior calls to complete
|
|
8156
|
+
await this.semaphore.acquire();
|
|
8157
|
+
try {
|
|
7807
8158
|
this.refill();
|
|
7808
|
-
|
|
7809
|
-
this.tokens
|
|
8159
|
+
while (this.tokens < 1) {
|
|
8160
|
+
const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;
|
|
8161
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
8162
|
+
this.refill();
|
|
8163
|
+
}
|
|
8164
|
+
this.tokens -= 1;
|
|
8165
|
+
}
|
|
8166
|
+
catch (error) {
|
|
8167
|
+
this.semaphore.release();
|
|
8168
|
+
throw error;
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
release() {
|
|
8172
|
+
this.semaphore.release();
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
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));
|
|
8195
|
+
}
|
|
8196
|
+
async acquire() {
|
|
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());
|
|
7810
8202
|
return;
|
|
7811
8203
|
}
|
|
7812
|
-
const
|
|
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
|
+
}
|
|
7813
8210
|
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
8211
|
+
}
|
|
8212
|
+
}
|
|
8213
|
+
release() {
|
|
8214
|
+
this.semaphore.release();
|
|
7818
8215
|
}
|
|
7819
8216
|
}
|
|
7820
8217
|
|
|
@@ -7832,8 +8229,12 @@
|
|
|
7832
8229
|
await this.throttleBucket.acquire();
|
|
7833
8230
|
}
|
|
7834
8231
|
}
|
|
8232
|
+
release() {
|
|
8233
|
+
if (this.throttleBucket) {
|
|
8234
|
+
this.throttleBucket.release();
|
|
8235
|
+
}
|
|
8236
|
+
}
|
|
7835
8237
|
async handle429(error, attempt) {
|
|
7836
|
-
// Remove the throw here; let invokeMethod handle exhaustion
|
|
7837
8238
|
const retryAfter = error.response.headers.get("Retry-After");
|
|
7838
8239
|
return retryAfter
|
|
7839
8240
|
? parseInt(retryAfter, 10) * 1000
|
|
@@ -7842,19 +8243,23 @@
|
|
|
7842
8243
|
}
|
|
7843
8244
|
|
|
7844
8245
|
function quickbase(config) {
|
|
7845
|
-
const { realm, userToken, tempToken, useTempTokens, useSso, samlToken, fetchApi, debug, convertDates = true, tempTokenLifespan = 290000, throttle = { rate:
|
|
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;
|
|
7846
8248
|
const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
|
|
7847
|
-
const
|
|
7848
|
-
|
|
7849
|
-
|
|
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);
|
|
7850
8257
|
const rateLimiter = new RateLimiter(throttleBucket, maxRetries, retryDelay);
|
|
7851
|
-
const defaultFetch =
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
const effectiveFetch = fetchApi || defaultFetch;
|
|
7855
|
-
if (!effectiveFetch) {
|
|
7856
|
-
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.");
|
|
7857
8261
|
}
|
|
8262
|
+
const effectiveFetch = fetchApi || defaultFetch;
|
|
7858
8263
|
const authStrategy = useSso
|
|
7859
8264
|
? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug, baseUrl)
|
|
7860
8265
|
: useTempTokens
|
|
@@ -7909,19 +8314,23 @@
|
|
|
7909
8314
|
const methodMap = buildMethodMap();
|
|
7910
8315
|
const proxyHandler = {
|
|
7911
8316
|
get: (_, prop) => {
|
|
7912
|
-
|
|
8317
|
+
if (debug) {
|
|
8318
|
+
console.log("[proxy] Accessing:", prop, "in methodMap:", prop in methodMap);
|
|
8319
|
+
}
|
|
7913
8320
|
if (prop in methodMap) {
|
|
7914
8321
|
const methodName = prop;
|
|
7915
|
-
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);
|
|
7916
8326
|
}
|
|
7917
|
-
console.log("[proxy] Method not found:", prop);
|
|
7918
8327
|
return undefined;
|
|
7919
8328
|
},
|
|
7920
8329
|
};
|
|
7921
8330
|
const proxy = new Proxy({}, proxyHandler);
|
|
7922
8331
|
if (debug) {
|
|
7923
8332
|
console.log("[createClient] Config:", config);
|
|
7924
|
-
console.log("[createClient]
|
|
8333
|
+
console.log("[createClient] Returning proxy");
|
|
7925
8334
|
}
|
|
7926
8335
|
return proxy;
|
|
7927
8336
|
}
|