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