quickbase-js 0.1.0-beta.10 → 0.1.0-beta.11
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 +9 -0
- package/README.md +49 -9
- package/dist/esm/quickbase.d.ts +1 -0
- package/dist/esm/quickbase.js +60 -64
- 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 +60 -64
- 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/examples/quicbase-jsCDN.html +2 -2
- package/package.json +1 -1
- package/src/authorizationStrategy.ts +62 -35
- package/src/generated-unified/QuickbaseClient.ts +1 -1
- package/src/invokeMethod.ts +23 -63
- package/src/quickbaseClient.ts +9 -4
- package/src/tokenCache.ts +4 -0
- package/tests/playwright/qb/auth/getAppWithTempTokenConcurrency.test.ts +208 -0
- package/tests/vitest/qb/auth/ssoTokenConcurrency.test.ts +68 -0
- package/tests/vitest/qb/auth/userTokenConcurrency.test.ts +63 -0
- package/tests/vitest/unit/apps/getAppConcurrency.test.ts +151 -0
- package/tests/vitest/unit/auth/fetchTempToken401.test.ts +20 -2
- package/tests/vitest/unit/auth/userTokenConcurrency.test.ts +62 -0
- package/tests/vitest/unit/index.ts +1 -1
- package/tests/vitest/unit/quickbaseClientHttpMethods.test.ts +3 -3
- package/tools/log-gen.ts +1 -1
- package/tools/log-gen.yaml +130 -78
- package/api-examples.md +0 -49
- package/markdown-examples.md +0 -85
|
@@ -7354,6 +7354,9 @@
|
|
|
7354
7354
|
expiresAt: now + (lifespan || this.tempTokenLifespan),
|
|
7355
7355
|
});
|
|
7356
7356
|
}
|
|
7357
|
+
delete(dbid) {
|
|
7358
|
+
this.cache.delete(dbid);
|
|
7359
|
+
}
|
|
7357
7360
|
clear() {
|
|
7358
7361
|
this.cache.clear();
|
|
7359
7362
|
}
|
|
@@ -7408,14 +7411,13 @@
|
|
|
7408
7411
|
return match ? match[1].toUpperCase() : "GET"; // Fallback to GET if no match
|
|
7409
7412
|
}
|
|
7410
7413
|
|
|
7411
|
-
// src/authorizationStrategy.ts
|
|
7412
|
-
// src/authorizationStrategy.ts (partial update)
|
|
7413
7414
|
class TempTokenStrategy {
|
|
7414
7415
|
tokenCache;
|
|
7415
7416
|
initialTempToken;
|
|
7416
7417
|
fetchApi;
|
|
7417
7418
|
realm;
|
|
7418
7419
|
baseUrl;
|
|
7420
|
+
pendingFetches = new Map();
|
|
7419
7421
|
constructor(tokenCache, initialTempToken, fetchApi, realm, baseUrl = "https://api.quickbase.com/v1") {
|
|
7420
7422
|
this.tokenCache = tokenCache;
|
|
7421
7423
|
this.initialTempToken = initialTempToken;
|
|
@@ -7450,7 +7452,13 @@
|
|
|
7450
7452
|
async getToken(dbid) {
|
|
7451
7453
|
let token = this.tokenCache.get(dbid) || this.initialTempToken;
|
|
7452
7454
|
if (!token && dbid) {
|
|
7453
|
-
|
|
7455
|
+
if (this.pendingFetches.has(dbid)) {
|
|
7456
|
+
console.log(`[getToken] Waiting for existing fetch for dbid: ${dbid}`);
|
|
7457
|
+
return this.pendingFetches.get(dbid);
|
|
7458
|
+
}
|
|
7459
|
+
const fetchPromise = this.fetchTempToken(dbid).finally(() => this.pendingFetches.delete(dbid));
|
|
7460
|
+
this.pendingFetches.set(dbid, fetchPromise);
|
|
7461
|
+
token = await fetchPromise;
|
|
7454
7462
|
}
|
|
7455
7463
|
return token;
|
|
7456
7464
|
}
|
|
@@ -7470,15 +7478,29 @@
|
|
|
7470
7478
|
}
|
|
7471
7479
|
if (debug)
|
|
7472
7480
|
console.log(`Refreshing temp token for dbid: ${dbid}`);
|
|
7473
|
-
|
|
7474
|
-
this.tokenCache.
|
|
7475
|
-
|
|
7481
|
+
// Invalidate the cache for this dbid to force a fresh fetch
|
|
7482
|
+
this.tokenCache.delete(dbid);
|
|
7483
|
+
try {
|
|
7484
|
+
const newToken = await this.getToken(dbid);
|
|
7485
|
+
if (newToken) {
|
|
7486
|
+
this.tokenCache.set(dbid, newToken);
|
|
7487
|
+
return newToken;
|
|
7488
|
+
}
|
|
7489
|
+
return null;
|
|
7490
|
+
}
|
|
7491
|
+
catch (error) {
|
|
7492
|
+
if (debug)
|
|
7493
|
+
console.log(`[${methodName || "method"}] Failed to refresh token:`, error);
|
|
7494
|
+
throw error;
|
|
7495
|
+
}
|
|
7476
7496
|
}
|
|
7477
7497
|
}
|
|
7478
7498
|
class UserTokenStrategy {
|
|
7479
7499
|
userToken;
|
|
7480
|
-
|
|
7500
|
+
baseUrl;
|
|
7501
|
+
constructor(userToken, baseUrl = "https://api.quickbase.com/v1") {
|
|
7481
7502
|
this.userToken = userToken;
|
|
7503
|
+
this.baseUrl = baseUrl;
|
|
7482
7504
|
}
|
|
7483
7505
|
async getToken(_dbid) {
|
|
7484
7506
|
return this.userToken;
|
|
@@ -7499,21 +7521,31 @@
|
|
|
7499
7521
|
realm;
|
|
7500
7522
|
fetchApi;
|
|
7501
7523
|
debug;
|
|
7524
|
+
baseUrl;
|
|
7502
7525
|
currentToken;
|
|
7503
|
-
|
|
7526
|
+
pendingFetches = new Map();
|
|
7527
|
+
constructor(samlToken, realm, fetchApi, debug = false, baseUrl = "https://api.quickbase.com/v1") {
|
|
7504
7528
|
this.samlToken = samlToken;
|
|
7505
7529
|
this.realm = realm;
|
|
7506
7530
|
this.fetchApi = fetchApi;
|
|
7507
7531
|
this.debug = debug;
|
|
7532
|
+
this.baseUrl = baseUrl;
|
|
7508
7533
|
}
|
|
7509
7534
|
async getToken(_dbid) {
|
|
7510
7535
|
if (!this.currentToken) {
|
|
7511
|
-
|
|
7536
|
+
if (this.pendingFetches.has("sso")) {
|
|
7537
|
+
if (this.debug)
|
|
7538
|
+
console.log("[getToken] Waiting for existing SSO fetch");
|
|
7539
|
+
return this.pendingFetches.get("sso");
|
|
7540
|
+
}
|
|
7541
|
+
const fetchPromise = this.fetchSsoToken({
|
|
7512
7542
|
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
7513
7543
|
requested_token_type: "urn:quickbase:params:oauth:token-type:temp_token",
|
|
7514
7544
|
subject_token: this.samlToken,
|
|
7515
7545
|
subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
|
|
7516
|
-
});
|
|
7546
|
+
}).finally(() => this.pendingFetches.delete("sso"));
|
|
7547
|
+
this.pendingFetches.set("sso", fetchPromise);
|
|
7548
|
+
this.currentToken = await fetchPromise;
|
|
7517
7549
|
}
|
|
7518
7550
|
return this.currentToken;
|
|
7519
7551
|
}
|
|
@@ -7540,7 +7572,7 @@
|
|
|
7540
7572
|
subject_token: this.samlToken,
|
|
7541
7573
|
subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
|
|
7542
7574
|
};
|
|
7543
|
-
const response = await this.fetchApi(
|
|
7575
|
+
const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
|
|
7544
7576
|
method: "POST",
|
|
7545
7577
|
headers: {
|
|
7546
7578
|
"QB-Realm-Hostname": `${this.realm}.quickbase.com`,
|
|
@@ -7567,7 +7599,7 @@
|
|
|
7567
7599
|
return newToken;
|
|
7568
7600
|
}
|
|
7569
7601
|
async fetchSsoToken(params) {
|
|
7570
|
-
const response = await this.fetchApi(
|
|
7602
|
+
const response = await this.fetchApi(`${this.baseUrl}/auth/oauth/token`, {
|
|
7571
7603
|
method: "POST",
|
|
7572
7604
|
headers: {
|
|
7573
7605
|
"QB-Realm-Hostname": `${this.realm}.quickbase.com`,
|
|
@@ -7615,8 +7647,7 @@
|
|
|
7615
7647
|
}
|
|
7616
7648
|
|
|
7617
7649
|
// src/invokeMethod.ts
|
|
7618
|
-
async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates) {
|
|
7619
|
-
console.log("[invokeMethod] Starting for method:", methodName);
|
|
7650
|
+
async function invokeMethod(methodName, params, methodMap, baseHeaders, authStrategy, rateLimiter, transformDates, debug, convertDates, attempt = 0, maxAttempts = rateLimiter.maxRetries + 1) {
|
|
7620
7651
|
const methodInfo = methodMap[methodName];
|
|
7621
7652
|
if (!methodInfo)
|
|
7622
7653
|
throw new Error(`Method ${methodName} not found`);
|
|
@@ -7634,33 +7665,21 @@
|
|
|
7634
7665
|
method: methodInfo.httpMethod,
|
|
7635
7666
|
};
|
|
7636
7667
|
let dbid = extractDbid(params);
|
|
7637
|
-
console.log("[invokeMethod] Extracted dbid:", dbid);
|
|
7638
7668
|
let token = dbid
|
|
7639
7669
|
? await authStrategy.getToken(dbid)
|
|
7640
7670
|
: await authStrategy.getToken("");
|
|
7641
|
-
console.log("[invokeMethod] Initial token:", token);
|
|
7642
7671
|
if (token) {
|
|
7643
7672
|
authStrategy.applyHeaders(baseHeaders, token);
|
|
7644
7673
|
requestOptions.headers = { ...baseHeaders };
|
|
7645
|
-
console.log("[invokeMethod] Token found, headers set:", requestOptions.headers);
|
|
7646
7674
|
if (methodName === "getTempTokenDBID") {
|
|
7647
|
-
console.log("[invokeMethod] Using cached token:", token);
|
|
7648
7675
|
return { temporaryAuthorization: token };
|
|
7649
7676
|
}
|
|
7650
7677
|
}
|
|
7651
|
-
if (debug) {
|
|
7652
|
-
console.log(`[${methodName}] requestParameters:`, requestParameters);
|
|
7653
|
-
console.log(`[${methodName}] requestOptions:`, requestOptions);
|
|
7654
|
-
}
|
|
7655
7678
|
async function processResponse(rawResponse) {
|
|
7656
|
-
if (debug)
|
|
7657
|
-
console.log(`[${methodName}] rawResponse:`, rawResponse);
|
|
7658
7679
|
if (rawResponse instanceof Response) {
|
|
7659
7680
|
const contentType = rawResponse.headers
|
|
7660
7681
|
.get("Content-Type")
|
|
7661
7682
|
?.toLowerCase();
|
|
7662
|
-
if (debug)
|
|
7663
|
-
console.log(`[${methodName}] contentType:`, contentType);
|
|
7664
7683
|
if (contentType?.includes("application/json")) {
|
|
7665
7684
|
const jsonResponse = await rawResponse.json();
|
|
7666
7685
|
return transformDates(jsonResponse, convertDates);
|
|
@@ -7669,8 +7688,6 @@
|
|
|
7669
7688
|
}
|
|
7670
7689
|
if (rawResponse && typeof rawResponse.value === "function") {
|
|
7671
7690
|
const response = await rawResponse.value();
|
|
7672
|
-
if (debug)
|
|
7673
|
-
console.log(`[${methodName}] Resolved JSONApiResponse:`, response);
|
|
7674
7691
|
return transformDates(response, convertDates);
|
|
7675
7692
|
}
|
|
7676
7693
|
return transformDates(rawResponse, convertDates);
|
|
@@ -7698,28 +7715,17 @@
|
|
|
7698
7715
|
}
|
|
7699
7716
|
}
|
|
7700
7717
|
catch (e) {
|
|
7701
|
-
if (debug)
|
|
7702
|
-
console.log(`[${methodName}] Error parsing response body:`, e);
|
|
7703
7718
|
message = "Failed to parse error response";
|
|
7704
7719
|
}
|
|
7705
|
-
if (debug)
|
|
7706
|
-
console.log("[invokeMethod] Parsed error - status:", status, "message:", message);
|
|
7707
7720
|
return { message, status };
|
|
7708
7721
|
}
|
|
7709
|
-
let attempt = 0;
|
|
7710
|
-
const maxAttempts = rateLimiter.maxRetries + 1;
|
|
7711
7722
|
while (attempt < maxAttempts) {
|
|
7712
|
-
console.log("[invokeMethod] Attempt:", attempt + 1, "of", maxAttempts);
|
|
7713
7723
|
try {
|
|
7714
7724
|
await rateLimiter.throttle();
|
|
7715
|
-
console.log("[invokeMethod] About to call API for method:", methodName);
|
|
7716
7725
|
const response = await methodInfo.method(requestParameters, requestOptions);
|
|
7717
|
-
console.log("[invokeMethod] API call completed for method:", methodName);
|
|
7718
7726
|
return await processResponse(response);
|
|
7719
7727
|
}
|
|
7720
7728
|
catch (error) {
|
|
7721
|
-
if (debug)
|
|
7722
|
-
console.log("[invokeMethod] Caught error:", error);
|
|
7723
7729
|
let status;
|
|
7724
7730
|
let message;
|
|
7725
7731
|
let response;
|
|
@@ -7732,31 +7738,13 @@
|
|
|
7732
7738
|
({ message, status } = await parseErrorResponse(response));
|
|
7733
7739
|
}
|
|
7734
7740
|
else {
|
|
7735
|
-
|
|
7736
|
-
console.log(`[${methodName}] Unexpected error:`, error);
|
|
7737
|
-
// Attempt to handle as a fetch error with a response
|
|
7738
|
-
if (error instanceof Error && "response" in error) {
|
|
7739
|
-
response = error.response;
|
|
7740
|
-
if (response instanceof Response) {
|
|
7741
|
-
({ message, status } = await parseErrorResponse(response));
|
|
7742
|
-
}
|
|
7743
|
-
else {
|
|
7744
|
-
throw error; // Rethrow if no response
|
|
7745
|
-
}
|
|
7746
|
-
}
|
|
7747
|
-
else {
|
|
7748
|
-
throw error; // Rethrow truly unexpected errors
|
|
7749
|
-
}
|
|
7741
|
+
throw error; // Rethrow truly unexpected errors
|
|
7750
7742
|
}
|
|
7751
|
-
if (debug)
|
|
7752
|
-
console.log("[invokeMethod] Handling error with status:", status);
|
|
7753
7743
|
if (status === 429) {
|
|
7754
7744
|
if (!(error instanceof ResponseError)) {
|
|
7755
7745
|
throw new Error("Expected ResponseError for 429 handling");
|
|
7756
7746
|
}
|
|
7757
7747
|
const delay = await rateLimiter.handle429(error, attempt + 1);
|
|
7758
|
-
if (debug)
|
|
7759
|
-
console.log(`[${methodName}] 429 delay: ${delay}ms`);
|
|
7760
7748
|
if (attempt + 1 === maxAttempts) {
|
|
7761
7749
|
throw new RateLimitError(`API Error: ${message} (Status: ${status})`, status, response?.headers.get("Retry-After")
|
|
7762
7750
|
? parseInt(response.headers.get("Retry-After"), 10)
|
|
@@ -7766,16 +7754,25 @@
|
|
|
7766
7754
|
attempt++;
|
|
7767
7755
|
continue;
|
|
7768
7756
|
}
|
|
7769
|
-
|
|
7770
|
-
|
|
7757
|
+
// Handle authentication errors
|
|
7758
|
+
let newToken;
|
|
7759
|
+
try {
|
|
7760
|
+
newToken = await authStrategy.handleError(status, params, attempt, maxAttempts, debug, methodName);
|
|
7761
|
+
}
|
|
7762
|
+
catch (authError) {
|
|
7763
|
+
throw authError; // Propagate the fetchTempToken error and exit immediately
|
|
7764
|
+
}
|
|
7765
|
+
// Only proceed with retry if we have a valid new token
|
|
7766
|
+
if (newToken !== null) {
|
|
7771
7767
|
token = newToken;
|
|
7772
7768
|
authStrategy.applyHeaders(baseHeaders, token);
|
|
7773
7769
|
requestOptions.headers = { ...baseHeaders };
|
|
7774
|
-
attempt++;
|
|
7775
7770
|
if (debug)
|
|
7776
7771
|
console.log(`[${methodName}] Retrying with token: ${token.substring(0, 10)}...`);
|
|
7772
|
+
attempt++;
|
|
7777
7773
|
continue;
|
|
7778
7774
|
}
|
|
7775
|
+
// If no new token was provided, throw the original error
|
|
7779
7776
|
throw new Error(`API Error: ${message} (Status: ${status})`);
|
|
7780
7777
|
}
|
|
7781
7778
|
}
|
|
@@ -7844,7 +7841,6 @@
|
|
|
7844
7841
|
}
|
|
7845
7842
|
}
|
|
7846
7843
|
|
|
7847
|
-
// src/quickbaseClient.ts
|
|
7848
7844
|
function quickbase(config) {
|
|
7849
7845
|
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;
|
|
7850
7846
|
const tokenCache = providedTokenCache || new TokenCache(tempTokenLifespan);
|
|
@@ -7860,10 +7856,10 @@
|
|
|
7860
7856
|
throw new Error("No fetch implementation available");
|
|
7861
7857
|
}
|
|
7862
7858
|
const authStrategy = useSso
|
|
7863
|
-
? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug)
|
|
7859
|
+
? new SsoTokenStrategy(samlToken || "", realm, effectiveFetch, debug, baseUrl)
|
|
7864
7860
|
: useTempTokens
|
|
7865
7861
|
? new TempTokenStrategy(tokenCache, tempToken, effectiveFetch, realm, baseUrl)
|
|
7866
|
-
: new UserTokenStrategy(userToken || "");
|
|
7862
|
+
: new UserTokenStrategy(userToken || "", baseUrl);
|
|
7867
7863
|
const baseHeaders = {
|
|
7868
7864
|
"QB-Realm-Hostname": `${realm}.quickbase.com`,
|
|
7869
7865
|
"Content-Type": "application/json",
|