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